Shrey Goel commited on
Commit ·
f211cf7
1
Parent(s): 0c19938
Add desolubilization, multipass, and olig tasks
Browse files- .gitignore +2 -2
- README.md +360 -12
- requirements.txt +176 -0
- src/configs/{guidance.yaml → desolubilize.yaml} +14 -13
- src/configs/lm.yaml +8 -8
- src/configs/multipass.yaml +70 -0
- src/configs/oligo.yaml +86 -0
- src/configs/solubility.yaml +75 -0
- src/guidance/multipass/dataloader.py +92 -0
- src/guidance/multipass/main.py +74 -0
- src/guidance/multipass/multipass_module.py +167 -0
- src/guidance/{utils.py → multipass/utils.py} +0 -0
- src/guidance/oligo/dataloader.py +91 -0
- src/guidance/oligo/main.py +74 -0
- src/guidance/oligo/oligo_module.py +155 -0
- src/guidance/oligo/utils.py +50 -0
- src/guidance/{dataloader.py → solubility/dataloader.py} +0 -0
- src/guidance/{main.py → solubility/main.py} +5 -4
- src/guidance/{solubility_module.py → solubility/solubility_module.py} +3 -3
- src/guidance/solubility/utils.py +21 -0
- src/lm/memdlm/diffusion_module.py +15 -11
- src/lm/memdlm/main.py +3 -2
- src/sampling/{guided_generator.py → desolubilize_generator.py} +49 -19
- src/sampling/multipass_generator.py +100 -0
- src/sampling/multipass_sampler.py +166 -0
- src/sampling/olig_generator.py +95 -0
- src/sampling/olig_sampler.py +187 -0
- src/sampling/pet_generator.py +121 -0
- src/sampling/{guided_sampler.py → pet_sampler.py} +33 -14
- src/sampling/unconditional_generator.py +25 -24
- src/sampling/unconditional_sampler.py +1 -1
- src/utils/__init__.py +0 -0
- src/utils/config_utils.py +15 -0
- src/utils/generate_utils.py +32 -7
.gitignore
CHANGED
|
@@ -13,12 +13,12 @@
|
|
| 13 |
/src/lm/evoflow_playground.ipynb
|
| 14 |
/src/utils/ubuntu_font
|
| 15 |
|
| 16 |
-
/src/sampling/old_guidance.py
|
| 17 |
-
|
| 18 |
/MeMDLM_v2.egg-info/
|
| 19 |
*.pth
|
| 20 |
*.ckpt
|
| 21 |
*.err
|
| 22 |
*.out
|
| 23 |
*.csv
|
|
|
|
|
|
|
| 24 |
__pycache__/
|
|
|
|
| 13 |
/src/lm/evoflow_playground.ipynb
|
| 14 |
/src/utils/ubuntu_font
|
| 15 |
|
|
|
|
|
|
|
| 16 |
/MeMDLM_v2.egg-info/
|
| 17 |
*.pth
|
| 18 |
*.ckpt
|
| 19 |
*.err
|
| 20 |
*.out
|
| 21 |
*.csv
|
| 22 |
+
*.ipynb
|
| 23 |
+
*.png
|
| 24 |
__pycache__/
|
README.md
CHANGED
|
@@ -2,25 +2,373 @@
|
|
| 2 |
|
| 3 |

|
| 4 |
|
|
|
|
| 5 |
|
| 6 |
-
|
| 7 |
|
| 8 |
-
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
-
|
| 13 |
-
- Higher BLOSUM-62 scores
|
| 14 |
-
- Improved pLDDT confidence
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
| 19 |
|
| 20 |
-
|
| 21 |
|
|
|
|
| 22 |
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |

|
| 4 |
|
| 5 |
+
**arXiv preprint:** [link TBD]
|
| 6 |
|
| 7 |
+
Reparameterized diffusion models (RDMs) have recently matched autoregressive methods in protein generation, motivating their use for challenging tasks such as designing membrane proteins, which possess interleaved soluble and transmembrane (TM) regions.
|
| 8 |
|
| 9 |
+
We introduce ***Membrane Diffusion Language Model (MemDLM)***, a fine-tuned RDM-based protein language model that enables controllable membrane protein sequence design. MemDLM-generated sequences recapitulate the TM residue density and structural features of natural membrane proteins, achieving comparable biological plausibility and outperforming state-of-the-art diffusion baselines in motif scaffolding tasks by producing:
|
| 10 |
|
| 11 |
+
- Lower perplexity
|
| 12 |
+
- Higher BLOSUM-62 scores
|
| 13 |
+
- Improved pLDDT confidence
|
| 14 |
|
| 15 |
+
To enhance controllability, we develop ***Per-Token Guidance (PET)***, a novel classifier-guided sampling strategy that selectively solubilizes residues while preserving conserved TM domains. This yields sequences with reduced TM density but intact functional cores.
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
Importantly, MemDLM designs validated in TOXCAT β-lactamase growth assays demonstrate successful TM insertion, distinguishing high-quality generated sequences from poor ones.
|
| 18 |
|
| 19 |
+
Together, our framework establishes the first experimentally validated diffusion-based model for rational membrane protein generation, integrating *de novo* design, motif scaffolding, and targeted property optimization.
|
| 20 |
|
| 21 |
+
---
|
| 22 |
|
| 23 |
+
## Repository Authors
|
| 24 |
|
| 25 |
+
- <u>[Shrey Goel](https://shreygoel09.github.io/)</u> – undergraduate student at Duke University
|
| 26 |
+
- <u>[Pranam Chatterjee](mailto:pranam@seas.upenn.edu)</u> – Assistant Professor at University of Pennsylvania
|
| 27 |
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
## Table of Contents
|
| 31 |
+
|
| 32 |
+
1. [Installation](#installation)
|
| 33 |
+
2. [Configuration](#configuration)
|
| 34 |
+
3. [Data Preparation](#data-preparation)
|
| 35 |
+
4. [Training](#training)
|
| 36 |
+
- [MeMDLM diffusion model](#1-memdlm-diffusion-model)
|
| 37 |
+
- [Solubility classifier](#2-solubility-classifier)
|
| 38 |
+
- [Oligomerization classifier](#3-oligomerization-classifier)
|
| 39 |
+
- [Multipass classifier](#4-multipass-classifier)
|
| 40 |
+
5. [Sampling](#sampling)
|
| 41 |
+
- [Unconditional generation](#1-unconditional-generation)
|
| 42 |
+
- [Solubilization (PET)](#2-solubilization-pet)
|
| 43 |
+
- [Desolubilization (PET)](#3-desolubilization-pet)
|
| 44 |
+
- [Multipass generation](#4-multipass-generation)
|
| 45 |
+
- [Oligomerization](#5-oligomerization)
|
| 46 |
+
6. [Citation](#citation)
|
| 47 |
+
|
| 48 |
+
---
|
| 49 |
+
|
| 50 |
+
## Installation
|
| 51 |
+
|
| 52 |
+
Clone the repository and install it in editable mode from the repository root:
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
git clone https://github.com/<your-org>/MeMDLM_v2.git
|
| 56 |
+
cd MeMDLM_v2
|
| 57 |
+
pip install -e .
|
| 58 |
+
pip install -r requirements.txt
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
Log in to Weights & Biases (used for experiment logging during training):
|
| 62 |
+
|
| 63 |
+
```bash
|
| 64 |
+
wandb login
|
| 65 |
+
# or: export WANDB_API_KEY=<your-key>
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
**Requirements:** CUDA-capable GPU(s), Python 3.10+, and sufficient disk space for pretrained checkpoints (EvoFlow-650M, ESM2-650M) and saved model weights.
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
## Configuration
|
| 73 |
+
|
| 74 |
+
All hyperparameters live in YAML files under `src/configs/`. Before running anything, set `base_dir` in each config to your local clone path:
|
| 75 |
+
|
| 76 |
+
```yaml
|
| 77 |
+
base_dir: /path/to/MeMDLM_v2
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
| Config file | Purpose |
|
| 81 |
+
|---|---|
|
| 82 |
+
| `src/configs/lm.yaml` | Fine-tune the MeMDLM diffusion language model |
|
| 83 |
+
| `src/configs/solubility.yaml` | Train/evaluate the per-residue solubility classifier (used for PET solubilization) |
|
| 84 |
+
| `src/configs/desolubilize.yaml` | PET desolubilization sampling settings (shares the solubility classifier checkpoint) |
|
| 85 |
+
| `src/configs/oligo.yaml` | Train/evaluate the oligomerization classifier |
|
| 86 |
+
| `src/configs/multipass.yaml` | Train/evaluate the multipass TM-segment classifier |
|
| 87 |
+
|
| 88 |
+
**Training modes** are controlled by `training.mode` in each config:
|
| 89 |
+
|
| 90 |
+
| Value | Behavior |
|
| 91 |
+
|---|---|
|
| 92 |
+
| `train` | Run training from scratch |
|
| 93 |
+
| `test` | Load `best_model.ckpt` and evaluate on the test set |
|
| 94 |
+
| `resume_from_checkpoint` | Resume MeMDLM training from `checkpointing.resume_ckpt_path` (`lm.yaml` only) |
|
| 95 |
+
|
| 96 |
+
Checkpoints are saved to `checkpoints/<wandb.name>/best_model.ckpt`. After training the diffusion model, ensure `lm.ft_evoflow` in the classifier and sampling configs matches the `wandb.name` from `lm.yaml`.
|
| 97 |
+
|
| 98 |
+
---
|
| 99 |
+
|
| 100 |
+
## Data Preparation
|
| 101 |
+
|
| 102 |
+
Place your datasets under `<base_dir>/data/`. Expected CSV formats:
|
| 103 |
+
|
| 104 |
+
### MeMDLM & solubility classifier (`data/train.csv`, `data/test.csv`, `data/val.csv`)
|
| 105 |
+
|
| 106 |
+
| Column | Description |
|
| 107 |
+
|---|---|
|
| 108 |
+
| `Sequence` | Amino acid sequence. For the solubility classifier, use **uppercase** for soluble residues and **lowercase** for TM residues. For MeMDLM pretraining, sequences are uppercased automatically. |
|
| 109 |
+
|
| 110 |
+
### Oligomerization classifier (`data/olig_clf/`)
|
| 111 |
+
|
| 112 |
+
| Column | Description |
|
| 113 |
+
|---|---|
|
| 114 |
+
| `Sequence` | Input sequence |
|
| 115 |
+
| `Binary Label` | `0` or `1` oligomerization label |
|
| 116 |
+
|
| 117 |
+
### Multipass classifier (`data/multipass/`)
|
| 118 |
+
|
| 119 |
+
| Column | Description |
|
| 120 |
+
|---|---|
|
| 121 |
+
| `Sequence` | Input sequence |
|
| 122 |
+
| `TM_segments` | Integer count of predicted TM segments |
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
+
|
| 126 |
+
## Training
|
| 127 |
+
|
| 128 |
+
All training jobs are launched from the **repository root**. We recommend running long jobs with `nohup` so they persist after logout. Create a `logs/` directory first:
|
| 129 |
+
|
| 130 |
+
```bash
|
| 131 |
+
mkdir -p logs
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
### 1. MeMDLM diffusion model
|
| 135 |
+
|
| 136 |
+
**Config:** `src/configs/lm.yaml`
|
| 137 |
+
**Entry point:** `src/lm/memdlm/main.py`
|
| 138 |
+
|
| 139 |
+
Set `training.mode: train` and adjust `training.devices` to the number of GPUs available.
|
| 140 |
+
|
| 141 |
+
```bash
|
| 142 |
+
nohup python src/lm/memdlm/main.py > logs/memdlm_train.out 2>&1 &
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
Key settings in `lm.yaml` (do not change unless reproducing a new experiment):
|
| 146 |
+
|
| 147 |
+
| Parameter | Default |
|
| 148 |
+
|---|---|
|
| 149 |
+
| Base model | `fredzzp/EvoFlow-650M-context-3070` |
|
| 150 |
+
| `training.max_steps` | 5000 |
|
| 151 |
+
| `training.n_layers` | 3 (unfrozen transformer layers) |
|
| 152 |
+
| `optim.lr` | 4e-5 |
|
| 153 |
+
| `data.batch_size` | 32 |
|
| 154 |
+
| `lm.num_diffusion_timesteps` | 500 |
|
| 155 |
+
|
| 156 |
+
Checkpoint saved to: `checkpoints/<wandb.name>/best_model.ckpt`
|
| 157 |
+
|
| 158 |
+
To evaluate a trained checkpoint, set `training.mode: test`.
|
| 159 |
+
|
| 160 |
+
```bash
|
| 161 |
+
nohup python src/lm/memdlm/main.py > logs/memdlm_test.out 2>&1 &
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
---
|
| 165 |
+
|
| 166 |
+
### 2. Solubility classifier
|
| 167 |
+
|
| 168 |
+
**Config:** `src/configs/solubility.yaml`
|
| 169 |
+
**Entry point:** `src/guidance/solubility/main.py`
|
| 170 |
+
|
| 171 |
+
Trains a per-residue ESM-based classifier that predicts soluble vs. TM residues. This checkpoint is required for PET solubilization and desolubilization sampling.
|
| 172 |
+
|
| 173 |
+
Set `training.mode: train`.
|
| 174 |
+
|
| 175 |
+
```bash
|
| 176 |
+
nohup python src/guidance/solubility/main.py > logs/solubility_train.out 2>&1 &
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
Key settings:
|
| 180 |
+
|
| 181 |
+
| Parameter | Default |
|
| 182 |
+
|---|---|
|
| 183 |
+
| `training.max_steps` | 3000 |
|
| 184 |
+
| `model.num_layers` | 4 |
|
| 185 |
+
| `optim.lr` | 3e-5 |
|
| 186 |
+
| `data.batch_size` | 32 |
|
| 187 |
+
|
| 188 |
+
Checkpoint saved to: `checkpoints/<wandb.name>/best_model.ckpt`
|
| 189 |
+
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
### 3. Oligomerization classifier
|
| 193 |
+
|
| 194 |
+
**Config:** `src/configs/oligo.yaml`
|
| 195 |
+
**Entry point:** `src/guidance/oligo/main.py`
|
| 196 |
+
|
| 197 |
+
Set `training.mode: train`.
|
| 198 |
+
|
| 199 |
+
```bash
|
| 200 |
+
nohup python src/guidance/oligo/main.py > logs/oligo_train.out 2>&1 &
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
Key settings:
|
| 204 |
+
|
| 205 |
+
| Parameter | Default |
|
| 206 |
+
|---|---|
|
| 207 |
+
| `training.max_steps` | 3000 |
|
| 208 |
+
| `model.num_layers` | 1 |
|
| 209 |
+
| `optim.lr` | 1e-4 |
|
| 210 |
+
| `data.batch_size` | 64 |
|
| 211 |
+
| `data.max_seq_len` | 54 |
|
| 212 |
+
|
| 213 |
+
Checkpoint saved to: `checkpoints/<wandb.name>/best_model.ckpt`
|
| 214 |
+
|
| 215 |
+
---
|
| 216 |
+
|
| 217 |
+
### 4. Multipass classifier
|
| 218 |
+
|
| 219 |
+
**Config:** `src/configs/multipass.yaml`
|
| 220 |
+
**Entry point:** `src/guidance/multipass/main.py`
|
| 221 |
+
|
| 222 |
+
Predicts the number of TM segments in a sequence. Required for multipass-guided generation.
|
| 223 |
+
|
| 224 |
+
Set `training.mode: train`.
|
| 225 |
+
|
| 226 |
+
```bash
|
| 227 |
+
nohup python src/guidance/multipass/main.py > logs/multipass_train.out 2>&1 &
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
Key settings:
|
| 231 |
+
|
| 232 |
+
| Parameter | Default |
|
| 233 |
+
|---|---|
|
| 234 |
+
| `training.max_steps` | 3000 |
|
| 235 |
+
| `model.num_layers` | 4 |
|
| 236 |
+
| `optim.lr` | 3e-5 |
|
| 237 |
+
| `data.batch_size` | 32 |
|
| 238 |
+
|
| 239 |
+
Checkpoint saved to: `checkpoints/<wandb.name>/best_model.ckpt`
|
| 240 |
+
|
| 241 |
+
---
|
| 242 |
+
|
| 243 |
+
### Recommended training order
|
| 244 |
+
|
| 245 |
+
```
|
| 246 |
+
1. MeMDLM (lm.yaml)
|
| 247 |
+
↓
|
| 248 |
+
2. Classifiers in any order:
|
| 249 |
+
• solubility.yaml
|
| 250 |
+
• oligo.yaml
|
| 251 |
+
• multipass.yaml
|
| 252 |
+
```
|
| 253 |
+
|
| 254 |
+
Update `lm.ft_evoflow` in all downstream configs to match the `wandb.name` produced by step 1.
|
| 255 |
+
|
| 256 |
+
---
|
| 257 |
+
|
| 258 |
+
## Sampling
|
| 259 |
+
|
| 260 |
+
Sampling scripts generate sequences and write CSVs under `<base_dir>/results/`. Each script loads the fine-tuned MeMDLM checkpoint (`checkpoints/<lm.ft_evoflow>/best_model.ckpt`) and, where applicable, the corresponding classifier checkpoint (`checkpoints/<wandb.name>/best_model.ckpt`).
|
| 261 |
+
|
| 262 |
+
Run all sampling jobs from the **repository root**:
|
| 263 |
+
|
| 264 |
+
```bash
|
| 265 |
+
mkdir -p logs
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
### 1. Unconditional generation
|
| 269 |
+
|
| 270 |
+
**Config:** `src/configs/lm.yaml`
|
| 271 |
+
**Script:** `src/sampling/unconditional_generator.py`
|
| 272 |
+
|
| 273 |
+
Generates *de novo* membrane protein sequences from a fully masked prior.
|
| 274 |
+
|
| 275 |
+
```bash
|
| 276 |
+
nohup python src/sampling/unconditional_generator.py > logs/unconditional_sample.out 2>&1 &
|
| 277 |
+
```
|
| 278 |
+
|
| 279 |
+
**Output:** `results/denovo/<wandb.name>/<date>_multipass/seqs_with_ppl.csv`
|
| 280 |
+
|
| 281 |
+
Columns: `Generated Sequence`, `ESM PPL`, `MeMDLM PPL`
|
| 282 |
+
|
| 283 |
+
---
|
| 284 |
+
|
| 285 |
+
### 2. Solubilization (PET)
|
| 286 |
+
|
| 287 |
+
**Config:** `src/configs/solubility.yaml`
|
| 288 |
+
**Script:** `src/sampling/pet_generator.py`
|
| 289 |
+
|
| 290 |
+
Uses Per-Token Guidance to redesign **uppercase (soluble)** positions in a scaffold while preserving **lowercase (TM)** residues. Update the input CSV path in `pet_generator.py` (default: `results/heme/cybtx.csv`) to point to your scaffold sequences.
|
| 291 |
+
|
| 292 |
+
```bash
|
| 293 |
+
nohup python src/sampling/pet_generator.py > logs/solubilize_sample.out 2>&1 &
|
| 294 |
+
```
|
| 295 |
+
|
| 296 |
+
**Output:** `results/heme/<lm.ft_evoflow>/solubilize/<date>/<prior-params>/infilled_seqs.csv`
|
| 297 |
+
|
| 298 |
+
---
|
| 299 |
+
|
| 300 |
+
### 3. Desolubilization (PET)
|
| 301 |
+
|
| 302 |
+
**Config:** `src/configs/desolubilize.yaml`
|
| 303 |
+
**Script:** `src/sampling/desolubilize_generator.py`
|
| 304 |
+
|
| 305 |
+
The inverse of solubilization: redesigns **lowercase (soluble)** positions while preserving **uppercase (TM)** residues. Uses the same solubility classifier checkpoint. Update the input CSV path in `desolubilize_generator.py` (default: `results/heme/4d2.csv`).
|
| 306 |
+
|
| 307 |
+
```bash
|
| 308 |
+
nohup python src/sampling/desolubilize_generator.py > logs/desolubilize_sample.out 2>&1 &
|
| 309 |
+
```
|
| 310 |
+
|
| 311 |
+
**Output:** `results/heme/<lm.ft_evoflow>/desolubilize/<date>/<prior-params>/infilled_seqs.csv`
|
| 312 |
+
|
| 313 |
+
---
|
| 314 |
+
|
| 315 |
+
### 4. Multipass generation
|
| 316 |
+
|
| 317 |
+
**Config:** `src/configs/multipass.yaml`
|
| 318 |
+
**Script:** `src/sampling/multipass_generator.py`
|
| 319 |
+
|
| 320 |
+
Generates sequences guided toward a target multipass TM topology using the multipass classifier.
|
| 321 |
+
|
| 322 |
+
```bash
|
| 323 |
+
nohup python src/sampling/multipass_generator.py > logs/multipass_sample.out 2>&1 &
|
| 324 |
+
```
|
| 325 |
+
|
| 326 |
+
**Output:** `results/multipass/<wandb.name>/<date>/lamb=<reg_strength>_tau=<sampling_temperature>/seqs_with_ppl.csv`
|
| 327 |
+
|
| 328 |
+
Columns: `Generated Sequence`, `ESM PPL`, `MeMDLM PPL`, `Pred TM Segments`
|
| 329 |
+
|
| 330 |
+
---
|
| 331 |
+
|
| 332 |
+
### 5. Oligomerization
|
| 333 |
+
|
| 334 |
+
**Config:** `src/configs/oligo.yaml`
|
| 335 |
+
**Script:** `src/sampling/olig_generator.py`
|
| 336 |
+
|
| 337 |
+
Redesigns sequences to increase predicted oligomerization propensity using the oligomerization classifier (NOS guidance).
|
| 338 |
+
|
| 339 |
+
```bash
|
| 340 |
+
nohup python src/sampling/olig_generator.py > logs/oligo_sample.out 2>&1 &
|
| 341 |
+
```
|
| 342 |
+
|
| 343 |
+
**Output:** `results/oligo/<wandb.name>/<date>/seqs_with_ppl.csv`
|
| 344 |
+
|
| 345 |
+
Columns: `Original Sequence`, `Generated Sequence`, `OG Olig Value`, `New Olig Value`, `Olig Increase`, `ESM PPL`, `MeMDLM PPL`, `MemDLM Blosum`
|
| 346 |
+
|
| 347 |
+
---
|
| 348 |
+
|
| 349 |
+
### Sampling quick-reference
|
| 350 |
+
|
| 351 |
+
| Task | Config | Script | Classifier required |
|
| 352 |
+
|---|---|---|---|
|
| 353 |
+
| Unconditional | `lm.yaml` | `unconditional_generator.py` | No |
|
| 354 |
+
| Solubilization | `solubility.yaml` | `pet_generator.py` | Solubility |
|
| 355 |
+
| Desolubilization | `desolubilize.yaml` | `desolubilize_generator.py` | Solubility |
|
| 356 |
+
| Multipass | `multipass.yaml` | `multipass_generator.py` | Multipass |
|
| 357 |
+
| Oligomerization | `oligo.yaml` | `olig_generator.py` | Oligomerization |
|
| 358 |
+
|
| 359 |
+
---
|
| 360 |
+
|
| 361 |
+
## Citation
|
| 362 |
+
|
| 363 |
+
If you use this repository in your research, please cite:
|
| 364 |
+
|
| 365 |
+
```bibtex
|
| 366 |
+
@article{goel2026memdlm,
|
| 367 |
+
title = {Token-Level Guided Discrete Diffusion for Membrane Protein Design},
|
| 368 |
+
author = {Goel, Shrey and Chatterjee, Pranam},
|
| 369 |
+
journal = {arXiv preprint arXiv:XXXX.XXXXX},
|
| 370 |
+
year = {2026}
|
| 371 |
+
}
|
| 372 |
+
```
|
| 373 |
+
|
| 374 |
+
If you enjoyed this repo, please cite it and star the repository. We appreciate your support!
|
requirements.txt
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
aiohappyeyeballs==2.6.1
|
| 2 |
+
aiohttp==3.12.15
|
| 3 |
+
aiosignal==1.4.0
|
| 4 |
+
annotated-types==0.7.0
|
| 5 |
+
antlr4-python3-runtime==4.9.3
|
| 6 |
+
anyio==4.10.0
|
| 7 |
+
argon2-cffi==25.1.0
|
| 8 |
+
argon2-cffi-bindings==25.1.0
|
| 9 |
+
arrow==1.3.0
|
| 10 |
+
asttokens==3.0.0
|
| 11 |
+
async-lru==2.0.5
|
| 12 |
+
attrs==25.3.0
|
| 13 |
+
babel==2.17.0
|
| 14 |
+
beautifulsoup4==4.13.5
|
| 15 |
+
bio==1.8.0
|
| 16 |
+
biopython==1.85
|
| 17 |
+
biothings_client==0.4.1
|
| 18 |
+
bleach==6.2.0
|
| 19 |
+
blosum==2.2.0
|
| 20 |
+
certifi==2025.8.3
|
| 21 |
+
cffi==2.0.0
|
| 22 |
+
charset-normalizer==3.4.3
|
| 23 |
+
click==8.3.0
|
| 24 |
+
comm==0.2.3
|
| 25 |
+
contourpy==1.3.3
|
| 26 |
+
cycler==0.12.1
|
| 27 |
+
datasets==4.4.2
|
| 28 |
+
debugpy==1.8.16
|
| 29 |
+
decorator==5.2.1
|
| 30 |
+
defusedxml==0.7.1
|
| 31 |
+
dill==0.4.0
|
| 32 |
+
executing==2.2.1
|
| 33 |
+
fastjsonschema==2.21.2
|
| 34 |
+
filelock==3.19.1
|
| 35 |
+
fonttools==4.61.1
|
| 36 |
+
fqdn==1.5.1
|
| 37 |
+
frozenlist==1.7.0
|
| 38 |
+
fsspec==2025.9.0
|
| 39 |
+
gitdb==4.0.12
|
| 40 |
+
GitPython==3.1.45
|
| 41 |
+
gprofiler-official==1.0.0
|
| 42 |
+
h11==0.16.0
|
| 43 |
+
hf-xet==1.1.10
|
| 44 |
+
httpcore==1.0.9
|
| 45 |
+
httpx==0.28.1
|
| 46 |
+
huggingface-hub==0.34.4
|
| 47 |
+
idna==3.10
|
| 48 |
+
ipykernel==6.30.1
|
| 49 |
+
ipython==9.5.0
|
| 50 |
+
ipython_pygments_lexers==1.1.1
|
| 51 |
+
isoduration==20.11.0
|
| 52 |
+
jedi==0.19.2
|
| 53 |
+
Jinja2==3.1.6
|
| 54 |
+
joblib==1.5.2
|
| 55 |
+
json5==0.12.1
|
| 56 |
+
jsonpointer==3.0.0
|
| 57 |
+
jsonschema==4.25.1
|
| 58 |
+
jsonschema-specifications==2025.9.1
|
| 59 |
+
jupyter_client==8.6.3
|
| 60 |
+
jupyter_core==5.8.1
|
| 61 |
+
jupyter-events==0.12.0
|
| 62 |
+
jupyter-lsp==2.3.0
|
| 63 |
+
jupyter_server==2.17.0
|
| 64 |
+
jupyter_server_terminals==0.5.3
|
| 65 |
+
jupyterlab==4.4.7
|
| 66 |
+
jupyterlab_pygments==0.3.0
|
| 67 |
+
jupyterlab_server==2.27.3
|
| 68 |
+
kiwisolver==1.4.9
|
| 69 |
+
lark==1.2.2
|
| 70 |
+
lightning==2.5.5
|
| 71 |
+
lightning-utilities==0.15.2
|
| 72 |
+
MarkupSafe==3.0.2
|
| 73 |
+
matplotlib==3.10.8
|
| 74 |
+
matplotlib-inline==0.1.7
|
| 75 |
+
MeMDLM_v2==1.0
|
| 76 |
+
mistune==3.1.4
|
| 77 |
+
mpmath==1.3.0
|
| 78 |
+
multidict==6.6.4
|
| 79 |
+
multiprocess==0.70.18
|
| 80 |
+
mygene==3.2.2
|
| 81 |
+
nbclient==0.10.2
|
| 82 |
+
nbconvert==7.16.6
|
| 83 |
+
nbformat==5.10.4
|
| 84 |
+
nest-asyncio==1.6.0
|
| 85 |
+
networkx==3.5
|
| 86 |
+
notebook==7.4.5
|
| 87 |
+
notebook_shim==0.2.4
|
| 88 |
+
numpy==2.3.3
|
| 89 |
+
nvidia-cublas-cu12==12.8.4.1
|
| 90 |
+
nvidia-cuda-cupti-cu12==12.8.90
|
| 91 |
+
nvidia-cuda-nvrtc-cu12==12.8.93
|
| 92 |
+
nvidia-cuda-runtime-cu12==12.8.90
|
| 93 |
+
nvidia-cudnn-cu12==9.10.2.21
|
| 94 |
+
nvidia-cufft-cu12==11.3.3.83
|
| 95 |
+
nvidia-cufile-cu12==1.13.1.3
|
| 96 |
+
nvidia-curand-cu12==10.3.9.90
|
| 97 |
+
nvidia-cusolver-cu12==11.7.3.90
|
| 98 |
+
nvidia-cusparse-cu12==12.5.8.93
|
| 99 |
+
nvidia-cusparselt-cu12==0.7.1
|
| 100 |
+
nvidia-nccl-cu12==2.27.3
|
| 101 |
+
nvidia-nvjitlink-cu12==12.8.93
|
| 102 |
+
nvidia-nvtx-cu12==12.8.90
|
| 103 |
+
omegaconf==2.3.0
|
| 104 |
+
packaging==25.0
|
| 105 |
+
pandas==2.3.2
|
| 106 |
+
pandocfilters==1.5.1
|
| 107 |
+
parso==0.8.5
|
| 108 |
+
pexpect==4.9.0
|
| 109 |
+
pillow==12.1.0
|
| 110 |
+
pip==25.2
|
| 111 |
+
platformdirs==4.4.0
|
| 112 |
+
pooch==1.8.2
|
| 113 |
+
prometheus_client==0.22.1
|
| 114 |
+
prompt_toolkit==3.0.52
|
| 115 |
+
propcache==0.3.2
|
| 116 |
+
protobuf==6.32.1
|
| 117 |
+
psutil==7.0.0
|
| 118 |
+
ptyprocess==0.7.0
|
| 119 |
+
pure_eval==0.2.3
|
| 120 |
+
pyarrow==22.0.0
|
| 121 |
+
pycparser==2.23
|
| 122 |
+
pydantic==2.11.9
|
| 123 |
+
pydantic_core==2.33.2
|
| 124 |
+
Pygments==2.19.2
|
| 125 |
+
pyparsing==3.3.2
|
| 126 |
+
python-dateutil==2.9.0.post0
|
| 127 |
+
python-json-logger==3.3.0
|
| 128 |
+
pytorch-lightning==2.5.5
|
| 129 |
+
pytz==2025.2
|
| 130 |
+
PyYAML==6.0.2
|
| 131 |
+
pyzmq==27.1.0
|
| 132 |
+
referencing==0.36.2
|
| 133 |
+
regex==2025.9.1
|
| 134 |
+
requests==2.32.5
|
| 135 |
+
rfc3339-validator==0.1.4
|
| 136 |
+
rfc3986-validator==0.1.1
|
| 137 |
+
rfc3987-syntax==1.1.0
|
| 138 |
+
rpds-py==0.27.1
|
| 139 |
+
safetensors==0.6.2
|
| 140 |
+
scikit-learn==1.7.2
|
| 141 |
+
scipy==1.16.2
|
| 142 |
+
seaborn==0.13.2
|
| 143 |
+
Send2Trash==1.8.3
|
| 144 |
+
sentry-sdk==2.38.0
|
| 145 |
+
setuptools==78.1.1
|
| 146 |
+
six==1.17.0
|
| 147 |
+
smmap==5.0.2
|
| 148 |
+
sniffio==1.3.1
|
| 149 |
+
soupsieve==2.8
|
| 150 |
+
stack-data==0.6.3
|
| 151 |
+
sympy==1.14.0
|
| 152 |
+
terminado==0.18.1
|
| 153 |
+
threadpoolctl==3.6.0
|
| 154 |
+
tinycss2==1.4.0
|
| 155 |
+
tokenizers==0.20.3
|
| 156 |
+
torch==2.8.0
|
| 157 |
+
torchmetrics==1.8.2
|
| 158 |
+
tornado==6.5.2
|
| 159 |
+
tqdm==4.67.1
|
| 160 |
+
traitlets==5.14.3
|
| 161 |
+
transformers==4.46.0
|
| 162 |
+
triton==3.4.0
|
| 163 |
+
types-python-dateutil==2.9.0.20250822
|
| 164 |
+
typing_extensions==4.15.0
|
| 165 |
+
typing-inspection==0.4.1
|
| 166 |
+
tzdata==2025.2
|
| 167 |
+
uri-template==1.3.0
|
| 168 |
+
urllib3==2.5.0
|
| 169 |
+
wandb==0.22.0
|
| 170 |
+
wcwidth==0.2.13
|
| 171 |
+
webcolors==24.11.1
|
| 172 |
+
webencodings==0.5.1
|
| 173 |
+
websocket-client==1.8.0
|
| 174 |
+
wheel==0.45.1
|
| 175 |
+
xxhash==3.6.0
|
| 176 |
+
yarl==1.20.1
|
src/configs/{guidance.yaml → desolubilize.yaml}
RENAMED
|
@@ -1,14 +1,14 @@
|
|
| 1 |
|
| 2 |
|
| 3 |
seed: 42
|
| 4 |
-
base_dir: /scratch/sgoel/MeMDLM_v2
|
| 5 |
|
| 6 |
|
| 7 |
lm:
|
| 8 |
pretrained_esm: facebook/esm2_t33_650M_UR50D
|
| 9 |
pretrained_evoflow: fredzzp/EvoFlow-650M-context-3070
|
| 10 |
pretrained_dplm: airkingbd/dplm_650m
|
| 11 |
-
ft_evoflow: ft_eflow-3070-650M_steps=
|
| 12 |
ft_dplm: ft_dplm-650M_steps=5k_layers=3_lr=0.00004_wd=.01_polynom_pwr=1_betas=.9-.98_bsz=32_gclip=1.0
|
| 13 |
|
| 14 |
model:
|
|
@@ -38,32 +38,33 @@ training:
|
|
| 38 |
val_check_interval: 250
|
| 39 |
enable_progress_bar: true
|
| 40 |
grad_clip_val: 1.0
|
| 41 |
-
devices:
|
| 42 |
|
| 43 |
guidance:
|
|
|
|
| 44 |
n_steps: 128
|
| 45 |
-
alpha:
|
| 46 |
gamma: 0.3
|
| 47 |
saliency_eps: 1e-4
|
| 48 |
saliency_t: 2.0
|
| 49 |
-
sampling_t: 0.
|
| 50 |
-
boltzmann_t: 0.
|
| 51 |
-
top_p: 0.
|
| 52 |
steps: 128
|
| 53 |
-
prior:
|
| 54 |
|
| 55 |
data:
|
| 56 |
batch_size: 32
|
| 57 |
max_seq_len: 1024
|
| 58 |
-
train: ${base_dir}/data/
|
| 59 |
-
test: ${base_dir}/data/
|
| 60 |
-
val: ${base_dir}/data/
|
| 61 |
|
| 62 |
|
| 63 |
wandb:
|
| 64 |
-
project:
|
| 65 |
group: programmablebio
|
| 66 |
-
name:
|
| 67 |
id: ${.name}_${seed}
|
| 68 |
|
| 69 |
|
|
|
|
| 1 |
|
| 2 |
|
| 3 |
seed: 42
|
| 4 |
+
base_dir: /scratch/pranamlab/sgoel/MeMDLM_v2
|
| 5 |
|
| 6 |
|
| 7 |
lm:
|
| 8 |
pretrained_esm: facebook/esm2_t33_650M_UR50D
|
| 9 |
pretrained_evoflow: fredzzp/EvoFlow-650M-context-3070
|
| 10 |
pretrained_dplm: airkingbd/dplm_650m
|
| 11 |
+
ft_evoflow: ft_eflow-3070-650M_steps=5k_layers=3_lr=1e-5_lr-end=2e-6_wd=.01_polynom_pwr=1_betas=.9-.98_bsz=8_gclip=1.0_ml=1024
|
| 12 |
ft_dplm: ft_dplm-650M_steps=5k_layers=3_lr=0.00004_wd=.01_polynom_pwr=1_betas=.9-.98_bsz=32_gclip=1.0
|
| 13 |
|
| 14 |
model:
|
|
|
|
| 38 |
val_check_interval: 250
|
| 39 |
enable_progress_bar: true
|
| 40 |
grad_clip_val: 1.0
|
| 41 |
+
devices: 2
|
| 42 |
|
| 43 |
guidance:
|
| 44 |
+
task: desolubilize
|
| 45 |
n_steps: 128
|
| 46 |
+
alpha: 5
|
| 47 |
gamma: 0.3
|
| 48 |
saliency_eps: 1e-4
|
| 49 |
saliency_t: 2.0
|
| 50 |
+
sampling_t: 0.8
|
| 51 |
+
boltzmann_t: 0.2
|
| 52 |
+
top_p: 0.3
|
| 53 |
steps: 128
|
| 54 |
+
prior: boltzmann
|
| 55 |
|
| 56 |
data:
|
| 57 |
batch_size: 32
|
| 58 |
max_seq_len: 1024
|
| 59 |
+
train: ${base_dir}/data/train.csv
|
| 60 |
+
test: ${base_dir}/data/test.csv
|
| 61 |
+
val: ${base_dir}/data/val.csv
|
| 62 |
|
| 63 |
|
| 64 |
wandb:
|
| 65 |
+
project: memdlm_again
|
| 66 |
group: programmablebio
|
| 67 |
+
name: clf_steps3k_lr3e-5_bsz32_heads2_drpt0.5_layers4
|
| 68 |
id: ${.name}_${seed}
|
| 69 |
|
| 70 |
|
src/configs/lm.yaml
CHANGED
|
@@ -17,7 +17,7 @@ optim:
|
|
| 17 |
type: adamw
|
| 18 |
scheduler: polynomial
|
| 19 |
lr: 0.00004
|
| 20 |
-
lr_end:
|
| 21 |
warmup_init_lr: 1e-07
|
| 22 |
weight_decay: 0.01
|
| 23 |
beta1: 0.9
|
|
@@ -35,24 +35,24 @@ training:
|
|
| 35 |
val_check_interval: 250
|
| 36 |
enable_progress_bar: true
|
| 37 |
grad_clip_val: 1.0
|
| 38 |
-
devices:
|
| 39 |
|
| 40 |
sampling:
|
| 41 |
n_steps: 128
|
| 42 |
|
| 43 |
|
| 44 |
data:
|
| 45 |
-
batch_size:
|
| 46 |
max_seq_len: 1024
|
| 47 |
-
train: ${base_dir}/data/
|
| 48 |
-
test: ${base_dir}/data/
|
| 49 |
-
val: ${base_dir}/data/
|
| 50 |
|
| 51 |
|
| 52 |
wandb:
|
| 53 |
-
project:
|
| 54 |
group: programmablebio
|
| 55 |
-
name: ft_eflow-3070-650M_steps=5k_layers=3_lr=
|
| 56 |
# name: ft_progen-base-764M_steps=50k_layers=2_lr=0.00004_wd=.1_cosine-to-frac_betas=.9-.999_bsz=8_gclip=0.8
|
| 57 |
# name: ft_dplm-650M_steps=5k_layers=3_lr=0.00004_wd=.01_polynom_pwr=1_betas=.9-.98_bsz=32_gclip=1.0
|
| 58 |
# name: ft_esm-650M_steps=3k_layers=3_lr=0.00004_wd=.01_polynom_pwr=1_betas=.9-.98_bsz=32_gclip=1.0
|
|
|
|
| 17 |
type: adamw
|
| 18 |
scheduler: polynomial
|
| 19 |
lr: 0.00004
|
| 20 |
+
lr_end: 2e-6
|
| 21 |
warmup_init_lr: 1e-07
|
| 22 |
weight_decay: 0.01
|
| 23 |
beta1: 0.9
|
|
|
|
| 35 |
val_check_interval: 250
|
| 36 |
enable_progress_bar: true
|
| 37 |
grad_clip_val: 1.0
|
| 38 |
+
devices: 3 # number of GPUs
|
| 39 |
|
| 40 |
sampling:
|
| 41 |
n_steps: 128
|
| 42 |
|
| 43 |
|
| 44 |
data:
|
| 45 |
+
batch_size: 32
|
| 46 |
max_seq_len: 1024
|
| 47 |
+
train: ${base_dir}/data/train.csv
|
| 48 |
+
test: ${base_dir}/data/test.csv
|
| 49 |
+
val: ${base_dir}/data/val.csv
|
| 50 |
|
| 51 |
|
| 52 |
wandb:
|
| 53 |
+
project: memdlm_again
|
| 54 |
group: programmablebio
|
| 55 |
+
name: ft_eflow-3070-650M_steps=5k_layers=3_lr=1e-5_lr-end=2e-6_wd=.01_polynom_pwr=1_betas=.9-.98_bsz=8_gclip=1.0_ml=1024
|
| 56 |
# name: ft_progen-base-764M_steps=50k_layers=2_lr=0.00004_wd=.1_cosine-to-frac_betas=.9-.999_bsz=8_gclip=0.8
|
| 57 |
# name: ft_dplm-650M_steps=5k_layers=3_lr=0.00004_wd=.01_polynom_pwr=1_betas=.9-.98_bsz=32_gclip=1.0
|
| 58 |
# name: ft_esm-650M_steps=3k_layers=3_lr=0.00004_wd=.01_polynom_pwr=1_betas=.9-.98_bsz=32_gclip=1.0
|
src/configs/multipass.yaml
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
seed: 42
|
| 4 |
+
base_dir: /scratch/pranamlab/sgoel/MeMDLM_v2
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
lm:
|
| 8 |
+
pretrained_esm: facebook/esm2_t33_650M_UR50D
|
| 9 |
+
pretrained_evoflow: fredzzp/EvoFlow-650M-context-3070
|
| 10 |
+
pretrained_dplm: airkingbd/dplm_650m
|
| 11 |
+
ft_evoflow: ft_eflow-3070-650M_steps=5k_layers=3_lr=1e-5_lr-end=2e-6_wd=.01_polynom_pwr=1_betas=.9-.98_bsz=8_gclip=1.0_ml=1024
|
| 12 |
+
ft_dplm: ft_dplm-650M_steps=5k_layers=3_lr=0.00004_wd=.01_polynom_pwr=1_betas=.9-.98_bsz=32_gclip=1.0
|
| 13 |
+
num_diffusion_timesteps: 500
|
| 14 |
+
|
| 15 |
+
model:
|
| 16 |
+
d_model: 1280
|
| 17 |
+
num_heads: 2
|
| 18 |
+
dropout: 0.5
|
| 19 |
+
num_layers: 4
|
| 20 |
+
label_pad_value: -100
|
| 21 |
+
|
| 22 |
+
optim:
|
| 23 |
+
type: adamw
|
| 24 |
+
lr: 3e-5
|
| 25 |
+
lr_end: 1e-5
|
| 26 |
+
weight_decay: 0.01
|
| 27 |
+
beta1: 0.9
|
| 28 |
+
beta2: 0.98
|
| 29 |
+
power: 1
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
training:
|
| 33 |
+
mode: test # train / test
|
| 34 |
+
n_layers: 4
|
| 35 |
+
max_steps: 3000
|
| 36 |
+
warmup_steps: 150
|
| 37 |
+
log_every_n_steps: 10
|
| 38 |
+
num_sanity_val_steps: 2
|
| 39 |
+
val_check_interval: 250
|
| 40 |
+
enable_progress_bar: true
|
| 41 |
+
grad_clip_val: 1.0
|
| 42 |
+
devices: 2
|
| 43 |
+
|
| 44 |
+
guidance:
|
| 45 |
+
guide_steps: 12
|
| 46 |
+
diffusion_steps: 128
|
| 47 |
+
reg_strength: 3 # 3 vs 0.1
|
| 48 |
+
step_size: 0.1
|
| 49 |
+
sampling_temperature: 1.15
|
| 50 |
+
|
| 51 |
+
data:
|
| 52 |
+
batch_size: 32
|
| 53 |
+
max_seq_len: 1024
|
| 54 |
+
train: ${base_dir}/data/multipass/train_tm_segment_counts.csv
|
| 55 |
+
test: ${base_dir}/data/multipass/test_tm_segment_counts.csv
|
| 56 |
+
val: ${base_dir}/data/multipass/val_tm_segment_counts.csv
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
wandb:
|
| 60 |
+
project: memdlm_again
|
| 61 |
+
group: programmablebio
|
| 62 |
+
name: multipass_clf_steps3k_lr3e-5_bsz32_heads2_drpt0.5_layers4
|
| 63 |
+
id: ${.name}_${seed}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
checkpointing:
|
| 67 |
+
save_every_n_steps: 250
|
| 68 |
+
save_dir: ${base_dir}/checkpoints/${wandb.name}
|
| 69 |
+
resume_ckpt_path: ${checkpointing.save_dir}/last.ckpt
|
| 70 |
+
best_ckpt_path: ${checkpointing.save_dir}/best_model.ckpt
|
src/configs/oligo.yaml
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
seed: 42
|
| 5 |
+
base_dir: /scratch/pranamlab/sgoel/MeMDLM_v2
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
lm:
|
| 9 |
+
pretrained_esm: facebook/esm2_t33_650M_UR50D
|
| 10 |
+
pretrained_evoflow: fredzzp/EvoFlow-650M-context-3070
|
| 11 |
+
pretrained_dplm: airkingbd/dplm_650m
|
| 12 |
+
ft_evoflow: ft_eflow-3070-650M_steps=5k_layers=3_lr=1e-5_lr-end=2e-6_wd=.01_polynom_pwr=1_betas=.9-.98_bsz=8_gclip=1.0_ml=1024
|
| 13 |
+
ft_dplm: ft_dplm-650M_steps=5k_layers=3_lr=0.00004_wd=.01_polynom_pwr=1_betas=.9-.98_bsz=32_gclip=1.0
|
| 14 |
+
|
| 15 |
+
model:
|
| 16 |
+
esm_dim: 1280
|
| 17 |
+
d_model: 128
|
| 18 |
+
num_heads: 2
|
| 19 |
+
dropout: 0.3
|
| 20 |
+
num_layers: 1
|
| 21 |
+
label_pad_value: -100
|
| 22 |
+
|
| 23 |
+
optim:
|
| 24 |
+
type: adamw
|
| 25 |
+
lr: 1e-4
|
| 26 |
+
lr_end: 1e-5
|
| 27 |
+
weight_decay: 0.02
|
| 28 |
+
beta1: 0.9
|
| 29 |
+
beta2: 0.98
|
| 30 |
+
power: 1
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
training:
|
| 34 |
+
mode: test # train / test
|
| 35 |
+
n_layers: 4
|
| 36 |
+
max_steps: 3000
|
| 37 |
+
warmup_steps: 200
|
| 38 |
+
log_every_n_steps: 10
|
| 39 |
+
num_sanity_val_steps: 2
|
| 40 |
+
val_check_interval: 100
|
| 41 |
+
enable_progress_bar: true
|
| 42 |
+
grad_clip_val: 1.0
|
| 43 |
+
devices: 2
|
| 44 |
+
|
| 45 |
+
guidance:
|
| 46 |
+
n_steps: 128
|
| 47 |
+
alpha: 3
|
| 48 |
+
gamma: 0.3
|
| 49 |
+
saliency_eps: 1e-4
|
| 50 |
+
saliency_t: 2.0
|
| 51 |
+
sampling_t: 0.7
|
| 52 |
+
boltzmann_t: 0.3
|
| 53 |
+
top_p: 0.2
|
| 54 |
+
steps: 128
|
| 55 |
+
prior: lm_probs # lm_probs / boltzmann
|
| 56 |
+
|
| 57 |
+
olig_guidance:
|
| 58 |
+
guide_steps: 12
|
| 59 |
+
diffusion_steps: 64
|
| 60 |
+
reg_strength: 1
|
| 61 |
+
step_size: 0.1
|
| 62 |
+
temperature: 1.0
|
| 63 |
+
eps: 1e-8
|
| 64 |
+
residue_thresh: 0.5
|
| 65 |
+
topk_frac: 0.2
|
| 66 |
+
|
| 67 |
+
data:
|
| 68 |
+
batch_size: 64
|
| 69 |
+
max_seq_len: 54
|
| 70 |
+
train: ${base_dir}/data/olig_clf/train.csv
|
| 71 |
+
test: ${base_dir}/data/olig_clf/test.csv
|
| 72 |
+
val: ${base_dir}/data/olig_clf/val.csv
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
wandb:
|
| 76 |
+
project: memdlm_oligo
|
| 77 |
+
group: programmablebio
|
| 78 |
+
name: oligo-clf_steps3k_lr1e-4_bsz64_drpt0.3_wd0.02_dmodel128_simple-mlp_mean-pool
|
| 79 |
+
id: ${.name}_${seed}
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
checkpointing:
|
| 83 |
+
save_every_n_steps: 250
|
| 84 |
+
save_dir: ${base_dir}/checkpoints/${wandb.name}
|
| 85 |
+
resume_ckpt_path: ${checkpointing.save_dir}/last.ckpt
|
| 86 |
+
best_ckpt_path: ${checkpointing.save_dir}/best_model.ckpt
|
src/configs/solubility.yaml
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
seed: 42
|
| 4 |
+
base_dir: /scratch/pranamlab/sgoel/MeMDLM_v2
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
lm:
|
| 8 |
+
pretrained_esm: facebook/esm2_t33_650M_UR50D
|
| 9 |
+
pretrained_evoflow: fredzzp/EvoFlow-650M-context-3070
|
| 10 |
+
pretrained_dplm: airkingbd/dplm_650m
|
| 11 |
+
ft_evoflow: ft_eflow-3070-650M_steps=5k_layers=3_lr=1e-5_lr-end=2e-6_wd=.01_polynom_pwr=1_betas=.9-.98_bsz=8_gclip=1.0_ml=1024
|
| 12 |
+
ft_dplm: ft_dplm-650M_steps=5k_layers=3_lr=0.00004_wd=.01_polynom_pwr=1_betas=.9-.98_bsz=32_gclip=1.0
|
| 13 |
+
|
| 14 |
+
model:
|
| 15 |
+
d_model: 1280
|
| 16 |
+
num_heads: 2
|
| 17 |
+
dropout: 0.5
|
| 18 |
+
num_layers: 4
|
| 19 |
+
label_pad_value: -100
|
| 20 |
+
|
| 21 |
+
optim:
|
| 22 |
+
type: adamw
|
| 23 |
+
lr: 3e-5
|
| 24 |
+
lr_end: 1e-5
|
| 25 |
+
weight_decay: 0.01
|
| 26 |
+
beta1: 0.9
|
| 27 |
+
beta2: 0.98
|
| 28 |
+
power: 1
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
training:
|
| 32 |
+
mode: test # train / test
|
| 33 |
+
n_layers: 4
|
| 34 |
+
max_steps: 3000
|
| 35 |
+
warmup_steps: 150
|
| 36 |
+
log_every_n_steps: 10
|
| 37 |
+
num_sanity_val_steps: 2
|
| 38 |
+
val_check_interval: 250
|
| 39 |
+
enable_progress_bar: true
|
| 40 |
+
grad_clip_val: 1.0
|
| 41 |
+
devices: 2
|
| 42 |
+
|
| 43 |
+
guidance:
|
| 44 |
+
task: solubilize
|
| 45 |
+
n_steps: 128
|
| 46 |
+
alpha: 5 # 3
|
| 47 |
+
gamma: 0.3
|
| 48 |
+
saliency_eps: 1e-4
|
| 49 |
+
saliency_t: 2.0
|
| 50 |
+
sampling_t: 1.0 # 0.7
|
| 51 |
+
boltzmann_t: 0.15 # 0.3
|
| 52 |
+
top_p: 0.3 # 0.2
|
| 53 |
+
steps: 128
|
| 54 |
+
prior: boltzmann
|
| 55 |
+
|
| 56 |
+
data:
|
| 57 |
+
batch_size: 32
|
| 58 |
+
max_seq_len: 1024
|
| 59 |
+
train: ${base_dir}/data/train.csv
|
| 60 |
+
test: ${base_dir}/data/test.csv
|
| 61 |
+
val: ${base_dir}/data/val.csv
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
wandb:
|
| 65 |
+
project: memdlm_again
|
| 66 |
+
group: programmablebio
|
| 67 |
+
name: clf_steps3k_lr3e-5_bsz32_heads2_drpt0.5_layers4
|
| 68 |
+
id: ${.name}_${seed}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
checkpointing:
|
| 72 |
+
save_every_n_steps: 250
|
| 73 |
+
save_dir: ${base_dir}/checkpoints/${wandb.name}
|
| 74 |
+
resume_ckpt_path: ${checkpointing.save_dir}/last.ckpt
|
| 75 |
+
best_ckpt_path: ${checkpointing.save_dir}/best_model.ckpt
|
src/guidance/multipass/dataloader.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import lightning.pytorch as pl
|
| 4 |
+
|
| 5 |
+
from transformers import AutoModel, AutoTokenizer
|
| 6 |
+
from torch.utils.data import Dataset, DataLoader
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class MembraneDataset(Dataset):
|
| 10 |
+
def __init__(self, config, data_path):
|
| 11 |
+
self.config = config
|
| 12 |
+
self.data = pd.read_csv(data_path)
|
| 13 |
+
self.tokenizer = AutoTokenizer.from_pretrained(self.config.lm.pretrained_esm)
|
| 14 |
+
|
| 15 |
+
def __len__(self):
|
| 16 |
+
return len(self.data)
|
| 17 |
+
|
| 18 |
+
def __getitem__(self, idx):
|
| 19 |
+
sequence = self.data.iloc[idx]["Sequence"]
|
| 20 |
+
tm_segs = self.data.iloc[idx]["TM_segments"]
|
| 21 |
+
|
| 22 |
+
tokens = self.tokenizer(
|
| 23 |
+
sequence.upper(),
|
| 24 |
+
return_tensors='pt',
|
| 25 |
+
padding='max_length',
|
| 26 |
+
truncation=True,
|
| 27 |
+
max_length=self.config.data.max_seq_len,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
labels = torch.tensor(1 if tm_segs > 0 else 0, dtype=torch.float)
|
| 31 |
+
|
| 32 |
+
return {
|
| 33 |
+
"input_ids": tokens['input_ids'],
|
| 34 |
+
"attention_mask": tokens['attention_mask'],
|
| 35 |
+
"labels": labels
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def collate_fn(batch):
|
| 40 |
+
input_ids = torch.stack([item['input_ids'].squeeze(0) for item in batch])
|
| 41 |
+
masks = torch.stack([item['attention_mask'].squeeze(0) for item in batch])
|
| 42 |
+
labels = torch.stack([item['labels'] for item in batch])
|
| 43 |
+
|
| 44 |
+
return {
|
| 45 |
+
'input_ids': input_ids,
|
| 46 |
+
'attention_mask': masks,
|
| 47 |
+
'labels': labels
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class MembraneDataModule(pl.LightningDataModule):
|
| 52 |
+
def __init__(self, config, train_dataset, val_dataset, test_dataset, collate_fn=collate_fn):
|
| 53 |
+
super().__init__()
|
| 54 |
+
self.train_dataset = train_dataset
|
| 55 |
+
self.val_dataset = val_dataset
|
| 56 |
+
self.test_dataset = test_dataset
|
| 57 |
+
self.collate_fn = collate_fn
|
| 58 |
+
self.batch_size = config.data.batch_size
|
| 59 |
+
|
| 60 |
+
def train_dataloader(self):
|
| 61 |
+
return DataLoader(self.train_dataset,
|
| 62 |
+
batch_size=self.batch_size,
|
| 63 |
+
collate_fn=self.collate_fn,
|
| 64 |
+
num_workers=8,
|
| 65 |
+
pin_memory=True)
|
| 66 |
+
|
| 67 |
+
def val_dataloader(self):
|
| 68 |
+
return DataLoader(self.val_dataset,
|
| 69 |
+
batch_size=self.batch_size,
|
| 70 |
+
collate_fn=self.collate_fn,
|
| 71 |
+
num_workers=8,
|
| 72 |
+
pin_memory=True)
|
| 73 |
+
|
| 74 |
+
def test_dataloader(self):
|
| 75 |
+
return DataLoader(self.test_dataset,
|
| 76 |
+
batch_size=self.batch_size,
|
| 77 |
+
collate_fn=self.collate_fn,
|
| 78 |
+
num_workers=8,
|
| 79 |
+
pin_memory=True)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def get_datasets(config):
|
| 83 |
+
"""Helper method to grab datasets to quickly init data module in main.py"""
|
| 84 |
+
train_dataset = MembraneDataset(config, config.data.train)
|
| 85 |
+
val_dataset = MembraneDataset(config, config.data.val)
|
| 86 |
+
test_dataset = MembraneDataset(config, config.data.test)
|
| 87 |
+
|
| 88 |
+
return {
|
| 89 |
+
"train": train_dataset,
|
| 90 |
+
"val": val_dataset,
|
| 91 |
+
"test": test_dataset
|
| 92 |
+
}
|
src/guidance/multipass/main.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import wandb
|
| 5 |
+
import lightning.pytorch as pl
|
| 6 |
+
|
| 7 |
+
from omegaconf import OmegaConf
|
| 8 |
+
from lightning.pytorch.loggers import WandbLogger
|
| 9 |
+
from lightning.pytorch.callbacks import ModelCheckpoint, LearningRateMonitor
|
| 10 |
+
|
| 11 |
+
from src.utils.model_utils import _print
|
| 12 |
+
from src.utils.config_utils import load_config
|
| 13 |
+
from src.guidance.multipass.multipass_module import MultipassClassifier
|
| 14 |
+
from src.guidance.multipass.dataloader import MembraneDataModule, get_datasets
|
| 15 |
+
from src.lm.memdlm.diffusion_module import MembraneDiffusion
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
config = load_config("multipass.yaml")
|
| 19 |
+
wandb.login()
|
| 20 |
+
|
| 21 |
+
# data
|
| 22 |
+
datasets = get_datasets(config)
|
| 23 |
+
data_module = MembraneDataModule(
|
| 24 |
+
config=config,
|
| 25 |
+
train_dataset=datasets['train'],
|
| 26 |
+
val_dataset=datasets['val'],
|
| 27 |
+
test_dataset=datasets['test'],
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
# wandb logging
|
| 31 |
+
#wandb.init(project=config.wandb.project, name=config.wandb.name)
|
| 32 |
+
wandb_logger = WandbLogger(**config.wandb)
|
| 33 |
+
|
| 34 |
+
# lightning checkpoints
|
| 35 |
+
lr_monitor = LearningRateMonitor(logging_interval="step")
|
| 36 |
+
checkpoint_callback = ModelCheckpoint(
|
| 37 |
+
monitor="val/loss",
|
| 38 |
+
save_top_k=1,
|
| 39 |
+
mode="min",
|
| 40 |
+
dirpath=config.checkpointing.save_dir,
|
| 41 |
+
filename="best_model",
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
# lightning trainer
|
| 45 |
+
trainer = pl.Trainer(
|
| 46 |
+
max_steps=config.training.max_steps,
|
| 47 |
+
accelerator="cuda",
|
| 48 |
+
devices=1, #config.training.devices if config.training.mode=='train' else [0],
|
| 49 |
+
#strategy=DDPStrategy(find_unused_parameters=True),
|
| 50 |
+
callbacks=[checkpoint_callback, lr_monitor],
|
| 51 |
+
logger=wandb_logger,
|
| 52 |
+
log_every_n_steps=config.training.log_every_n_steps
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
# Folder to save checkpoints
|
| 56 |
+
ckpt_dir = config.checkpointing.save_dir
|
| 57 |
+
os.makedirs(ckpt_dir, exist_ok=True)
|
| 58 |
+
|
| 59 |
+
# instantiate model
|
| 60 |
+
model = MultipassClassifier(config, MembraneDiffusion(config))
|
| 61 |
+
|
| 62 |
+
# train or evalute the model
|
| 63 |
+
if config.training.mode == "train":
|
| 64 |
+
trainer.fit(model, datamodule=data_module)
|
| 65 |
+
|
| 66 |
+
elif config.training.mode == "test":
|
| 67 |
+
ckpt_path = os.path.join(ckpt_dir, "best_model.ckpt")
|
| 68 |
+
state_dict = model.get_state_dict(ckpt_path)
|
| 69 |
+
model.load_state_dict(state_dict)
|
| 70 |
+
trainer.test(model, datamodule=data_module, ckpt_path=ckpt_path)
|
| 71 |
+
else:
|
| 72 |
+
raise ValueError(f"{config.training.mode} is invalid. Must be 'train' or 'test'")
|
| 73 |
+
|
| 74 |
+
wandb.finish()
|
src/guidance/multipass/multipass_module.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gc
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import lightning.pytorch as pl
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
|
| 7 |
+
from omegaconf import OmegaConf
|
| 8 |
+
|
| 9 |
+
from src.utils.model_utils import _print
|
| 10 |
+
from src.guidance.solubility.utils import CosineWarmup
|
| 11 |
+
|
| 12 |
+
from sklearn.metrics import roc_auc_score, accuracy_score
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
config = OmegaConf.load("/scratch/pranamlab/sgoel/MeMDLM_v2/src/configs/multipass.yaml")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class MultipassClassifier(pl.LightningModule):
|
| 19 |
+
def __init__(self, config, diffusion_model):
|
| 20 |
+
super().__init__()
|
| 21 |
+
self.config = config
|
| 22 |
+
self.loss_fn = nn.BCEWithLogitsLoss(reduction='none')
|
| 23 |
+
self.all_preds = []
|
| 24 |
+
self.all_labels = []
|
| 25 |
+
|
| 26 |
+
self.diffusion_model = diffusion_model
|
| 27 |
+
for p in self.diffusion_model.model.parameters():
|
| 28 |
+
p.requires_grad = False
|
| 29 |
+
self.diffusion_model.eval()
|
| 30 |
+
|
| 31 |
+
encoder_layer = nn.TransformerEncoderLayer(
|
| 32 |
+
d_model=config.model.d_model,
|
| 33 |
+
nhead=config.model.num_heads,
|
| 34 |
+
dropout=config.model.dropout,
|
| 35 |
+
batch_first=True
|
| 36 |
+
)
|
| 37 |
+
self.encoder = nn.TransformerEncoder(encoder_layer, config.model.num_layers)
|
| 38 |
+
self.layer_norm = nn.LayerNorm(config.model.d_model)
|
| 39 |
+
self.dropout = nn.Dropout(config.model.dropout)
|
| 40 |
+
self.mlp = nn.Sequential(
|
| 41 |
+
nn.Linear(config.model.d_model, config.model.d_model // 2),
|
| 42 |
+
nn.ReLU(),
|
| 43 |
+
nn.Dropout(config.model.dropout),
|
| 44 |
+
nn.Linear(config.model.d_model // 2, 1),
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# -------# Classifier step #-------- #
|
| 49 |
+
def forward(self, x_t, attn_mask, embeds=None, with_hidden=None):
|
| 50 |
+
if embeds is None:
|
| 51 |
+
with torch.no_grad():
|
| 52 |
+
embeds = self.diffusion_model.forward(
|
| 53 |
+
input_ids=x_t,
|
| 54 |
+
attention_mask=attn_mask,
|
| 55 |
+
return_hidden=True
|
| 56 |
+
)
|
| 57 |
+
else:
|
| 58 |
+
assert with_hidden is not None
|
| 59 |
+
|
| 60 |
+
encodings = self.encoder(embeds, src_key_padding_mask=(attn_mask== 0))
|
| 61 |
+
encodings = self.dropout(self.layer_norm(encodings))
|
| 62 |
+
mask = attn_mask.unsqueeze(-1)
|
| 63 |
+
pooled = (encodings * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1)
|
| 64 |
+
logits = self.mlp(pooled).squeeze(-1)
|
| 65 |
+
return logits
|
| 66 |
+
|
| 67 |
+
def step(self, batch):
|
| 68 |
+
input_ids = batch['input_ids']
|
| 69 |
+
attention_mask = batch['attention_mask']
|
| 70 |
+
labels = batch['labels']
|
| 71 |
+
|
| 72 |
+
t1 = self.diffusion_model.sample_t(input_ids)
|
| 73 |
+
maskable = self.diffusion_model.is_maskable(input_ids)
|
| 74 |
+
|
| 75 |
+
x_t, _ = self.diffusion_model.noise_x0(input_ids, t1, maskable_mask=maskable)
|
| 76 |
+
|
| 77 |
+
logits = self.forward(x_t, attention_mask)
|
| 78 |
+
loss = self.compute_loss(logits, labels)
|
| 79 |
+
|
| 80 |
+
return loss, logits
|
| 81 |
+
|
| 82 |
+
# -------# Training / Evaluation #-------- #
|
| 83 |
+
def training_step(self, batch, batch_idx):
|
| 84 |
+
train_loss, _ = self.step(batch)
|
| 85 |
+
self.log(name="train/loss", value=train_loss.item(), on_step=True, on_epoch=False, logger=True, sync_dist=True)
|
| 86 |
+
self.save_ckpt()
|
| 87 |
+
return train_loss
|
| 88 |
+
|
| 89 |
+
def validation_step(self, batch, batch_idx):
|
| 90 |
+
val_loss, _ = self.step(batch)
|
| 91 |
+
self.log(name="val/loss", value=val_loss.item(), on_step=False, on_epoch=True, logger=True, sync_dist=True)
|
| 92 |
+
return val_loss
|
| 93 |
+
|
| 94 |
+
def test_step(self, batch):
|
| 95 |
+
test_loss, logits = self.step(batch)
|
| 96 |
+
preds = F.sigmoid(logits)
|
| 97 |
+
self.all_preds.append(preds.detach().cpu())
|
| 98 |
+
self.all_labels.append(batch['labels'].detach().cpu())
|
| 99 |
+
self.log(name="test/loss", value=test_loss.item(), on_step=False, on_epoch=True, logger=True, sync_dist=True)
|
| 100 |
+
return test_loss
|
| 101 |
+
|
| 102 |
+
def on_test_epoch_start(self):
|
| 103 |
+
self.all_preds = []
|
| 104 |
+
self.all_labels = []
|
| 105 |
+
|
| 106 |
+
def on_test_epoch_end(self):
|
| 107 |
+
all_preds = torch.cat(self.all_preds).numpy()
|
| 108 |
+
all_labels = torch.cat(self.all_labels).numpy()
|
| 109 |
+
|
| 110 |
+
auroc = roc_auc_score(all_labels, all_preds)
|
| 111 |
+
binaries = (all_preds > 0.5).astype(int)
|
| 112 |
+
accuracy = accuracy_score(all_labels, binaries)
|
| 113 |
+
|
| 114 |
+
self.log(name="test/AUROC", value=auroc, on_step=False, on_epoch=True, logger=True, sync_dist=True)
|
| 115 |
+
self.log(name="test/accuracy", value=accuracy, on_step=False, on_epoch=True, logger=True, sync_dist=True)
|
| 116 |
+
|
| 117 |
+
def optimizer_step(self, *args, **kwargs):
|
| 118 |
+
super().optimizer_step(*args, **kwargs)
|
| 119 |
+
gc.collect()
|
| 120 |
+
torch.cuda.empty_cache()
|
| 121 |
+
|
| 122 |
+
def configure_optimizers(self):
|
| 123 |
+
path = self.config.training
|
| 124 |
+
optimizer = torch.optim.AdamW(self.parameters(), lr=self.config.optim.lr)
|
| 125 |
+
lr_scheduler = CosineWarmup(
|
| 126 |
+
optimizer,
|
| 127 |
+
warmup_steps=path.warmup_steps,
|
| 128 |
+
total_steps=path.max_steps,
|
| 129 |
+
)
|
| 130 |
+
scheduler_dict = {
|
| 131 |
+
"scheduler": lr_scheduler,
|
| 132 |
+
"interval": 'step',
|
| 133 |
+
'frequency': 1,
|
| 134 |
+
'monitor': 'val/loss',
|
| 135 |
+
'name': 'learning_rate'
|
| 136 |
+
}
|
| 137 |
+
return [optimizer], [scheduler_dict]
|
| 138 |
+
|
| 139 |
+
def save_ckpt(self):
|
| 140 |
+
curr_step = self.global_step
|
| 141 |
+
save_every = self.config.training.val_check_interval
|
| 142 |
+
if curr_step % save_every == 0 and curr_step > 0: # Save every 250 steps
|
| 143 |
+
ckpt_path = f"{self.config.checkpointing.save_dir}/step={curr_step}.ckpt"
|
| 144 |
+
self.trainer.save_checkpoint(ckpt_path)
|
| 145 |
+
|
| 146 |
+
# -------# Loss and Test Set Metrics #-------- #
|
| 147 |
+
def compute_loss(self, logits, labels):
|
| 148 |
+
"""Helper method to handle loss calculation"""
|
| 149 |
+
loss = self.loss_fn(logits, labels.float()).mean()
|
| 150 |
+
return loss
|
| 151 |
+
|
| 152 |
+
# -------# Helper Functions #-------- #
|
| 153 |
+
def get_state_dict(self, ckpt_path):
|
| 154 |
+
"""Helper method to load and process a trained model's state dict from saved checkpoint"""
|
| 155 |
+
def remove_model_prefix(state_dict):
|
| 156 |
+
for k in state_dict.keys():
|
| 157 |
+
if "model." in k:
|
| 158 |
+
k.replace('model.', '')
|
| 159 |
+
return state_dict
|
| 160 |
+
|
| 161 |
+
checkpoint = torch.load(ckpt_path, weights_only=False)#, map_location='cuda' if torch.cuda.is_available() else 'cpu')
|
| 162 |
+
state_dict = checkpoint.get("state_dict", checkpoint)
|
| 163 |
+
|
| 164 |
+
if any(k.startswith("model.") for k in state_dict.keys()):
|
| 165 |
+
state_dict = remove_model_prefix(state_dict)
|
| 166 |
+
|
| 167 |
+
return state_dict
|
src/guidance/{utils.py → multipass/utils.py}
RENAMED
|
File without changes
|
src/guidance/oligo/dataloader.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import lightning.pytorch as pl
|
| 4 |
+
|
| 5 |
+
from transformers import AutoTokenizer
|
| 6 |
+
from torch.utils.data import Dataset, DataLoader
|
| 7 |
+
|
| 8 |
+
class OligomerDataset(Dataset):
|
| 9 |
+
def __init__(self, config, data_path):
|
| 10 |
+
self.config = config
|
| 11 |
+
self.data = pd.read_csv(data_path)
|
| 12 |
+
self.tokenizer = AutoTokenizer.from_pretrained(self.config.lm.pretrained_esm)
|
| 13 |
+
|
| 14 |
+
def __len__(self):
|
| 15 |
+
return len(self.data)
|
| 16 |
+
|
| 17 |
+
def __getitem__(self, idx):
|
| 18 |
+
sequence = self.data.iloc[idx]["Sequence"]
|
| 19 |
+
|
| 20 |
+
tokens = self.tokenizer(
|
| 21 |
+
sequence,
|
| 22 |
+
return_tensors='pt',
|
| 23 |
+
padding='max_length',
|
| 24 |
+
truncation=True,
|
| 25 |
+
max_length=self.config.data.max_seq_len,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
labels = torch.tensor(self.data.iloc[idx]['Binary Label'], dtype=torch.float)
|
| 29 |
+
|
| 30 |
+
return {
|
| 31 |
+
"input_ids": tokens['input_ids'],
|
| 32 |
+
"attention_mask": tokens['attention_mask'],
|
| 33 |
+
"labels": labels
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def collate_fn(batch):
|
| 38 |
+
input_ids = torch.stack([item['input_ids'].squeeze(0) for item in batch])
|
| 39 |
+
masks = torch.stack([item['attention_mask'].squeeze(0) for item in batch])
|
| 40 |
+
labels = torch.stack([item['labels'] for item in batch])
|
| 41 |
+
|
| 42 |
+
return {
|
| 43 |
+
'input_ids': input_ids,
|
| 44 |
+
'attention_mask': masks,
|
| 45 |
+
'labels': labels
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class OligomerDataModule(pl.LightningDataModule):
|
| 50 |
+
def __init__(self, config, train_dataset, val_dataset, test_dataset, collate_fn=collate_fn):
|
| 51 |
+
super().__init__()
|
| 52 |
+
self.train_dataset = train_dataset
|
| 53 |
+
self.val_dataset = val_dataset
|
| 54 |
+
self.test_dataset = test_dataset
|
| 55 |
+
self.collate_fn = collate_fn
|
| 56 |
+
self.batch_size = config.data.batch_size
|
| 57 |
+
|
| 58 |
+
def train_dataloader(self):
|
| 59 |
+
return DataLoader(self.train_dataset,
|
| 60 |
+
batch_size=self.batch_size,
|
| 61 |
+
collate_fn=self.collate_fn,
|
| 62 |
+
num_workers=8,
|
| 63 |
+
pin_memory=True,
|
| 64 |
+
shuffle=True)
|
| 65 |
+
|
| 66 |
+
def val_dataloader(self):
|
| 67 |
+
return DataLoader(self.val_dataset,
|
| 68 |
+
batch_size=self.batch_size,
|
| 69 |
+
collate_fn=self.collate_fn,
|
| 70 |
+
num_workers=8,
|
| 71 |
+
pin_memory=True)
|
| 72 |
+
|
| 73 |
+
def test_dataloader(self):
|
| 74 |
+
return DataLoader(self.test_dataset,
|
| 75 |
+
batch_size=self.batch_size,
|
| 76 |
+
collate_fn=self.collate_fn,
|
| 77 |
+
num_workers=8,
|
| 78 |
+
pin_memory=True)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def get_datasets(config):
|
| 82 |
+
"""Helper method to grab datasets to quickly init data module in main.py"""
|
| 83 |
+
train_dataset = OligomerDataset(config, config.data.train)
|
| 84 |
+
val_dataset = OligomerDataset(config, config.data.val)
|
| 85 |
+
test_dataset = OligomerDataset(config, config.data.test)
|
| 86 |
+
|
| 87 |
+
return {
|
| 88 |
+
"train": train_dataset,
|
| 89 |
+
"val": val_dataset,
|
| 90 |
+
"test": test_dataset
|
| 91 |
+
}
|
src/guidance/oligo/main.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import wandb
|
| 5 |
+
import lightning.pytorch as pl
|
| 6 |
+
|
| 7 |
+
from omegaconf import OmegaConf
|
| 8 |
+
from lightning.pytorch.strategies import DDPStrategy
|
| 9 |
+
from lightning.pytorch.loggers import WandbLogger
|
| 10 |
+
from lightning.pytorch.callbacks import ModelCheckpoint, LearningRateMonitor
|
| 11 |
+
|
| 12 |
+
from src.utils.model_utils import _print
|
| 13 |
+
from src.utils.config_utils import load_config
|
| 14 |
+
from src.guidance.oligo.oligo_module import OligomerClassifier
|
| 15 |
+
from src.guidance.oligo.dataloader import get_datasets, OligomerDataModule
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
config = load_config("oligo.yaml")
|
| 19 |
+
wandb.login()
|
| 20 |
+
|
| 21 |
+
# data
|
| 22 |
+
datasets = get_datasets(config)
|
| 23 |
+
data_module = OligomerDataModule(
|
| 24 |
+
config=config,
|
| 25 |
+
train_dataset=datasets['train'],
|
| 26 |
+
val_dataset=datasets['val'],
|
| 27 |
+
test_dataset=datasets['test'],
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
# wandb logging
|
| 31 |
+
#wandb.init(project=config.wandb.project, name=config.wandb.name)
|
| 32 |
+
wandb_logger = WandbLogger(**config.wandb)
|
| 33 |
+
|
| 34 |
+
# lightning checkpoints
|
| 35 |
+
lr_monitor = LearningRateMonitor(logging_interval="step")
|
| 36 |
+
checkpoint_callback = ModelCheckpoint(
|
| 37 |
+
monitor="val/loss",
|
| 38 |
+
save_top_k=1,
|
| 39 |
+
mode="min",
|
| 40 |
+
dirpath=config.checkpointing.save_dir,
|
| 41 |
+
filename="best_model",
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
# lightning trainer
|
| 45 |
+
trainer = pl.Trainer(
|
| 46 |
+
max_steps=config.training.max_steps,
|
| 47 |
+
accelerator="cuda",
|
| 48 |
+
devices=1, #config.training.devices if config.training.mode=='train' else [0],
|
| 49 |
+
#strategy=DDPStrategy(find_unused_parameters=True),
|
| 50 |
+
callbacks=[checkpoint_callback, lr_monitor],
|
| 51 |
+
logger=wandb_logger,
|
| 52 |
+
log_every_n_steps=config.training.log_every_n_steps
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
# Folder to save checkpoints
|
| 56 |
+
ckpt_dir = config.checkpointing.save_dir
|
| 57 |
+
os.makedirs(ckpt_dir, exist_ok=True)
|
| 58 |
+
|
| 59 |
+
# instantiate model
|
| 60 |
+
model = OligomerClassifier(config)
|
| 61 |
+
|
| 62 |
+
# train or evalute the model
|
| 63 |
+
if config.training.mode == "train":
|
| 64 |
+
trainer.fit(model, datamodule=data_module)
|
| 65 |
+
|
| 66 |
+
elif config.training.mode == "test":
|
| 67 |
+
ckpt_path = os.path.join(ckpt_dir, "best_model.ckpt")
|
| 68 |
+
state_dict = model.get_state_dict(ckpt_path)
|
| 69 |
+
model.load_state_dict(state_dict)
|
| 70 |
+
trainer.test(model, datamodule=data_module, ckpt_path=ckpt_path)
|
| 71 |
+
else:
|
| 72 |
+
raise ValueError(f"{config.training.mode} is invalid. Must be 'train' or 'test'")
|
| 73 |
+
|
| 74 |
+
wandb.finish()
|
src/guidance/oligo/oligo_module.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gc
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import lightning.pytorch as pl
|
| 5 |
+
|
| 6 |
+
from omegaconf import OmegaConf
|
| 7 |
+
from transformers import AutoModel
|
| 8 |
+
from torchmetrics.classification import BinaryAUROC, BinaryAccuracy
|
| 9 |
+
|
| 10 |
+
from src.utils.model_utils import _print
|
| 11 |
+
from src.guidance.oligo.utils import CosineWarmup, HelixRoPE
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
config = OmegaConf.load("/scratch/pranamlab/sgoel/MeMDLM_v2/src/configs/oligo.yaml")
|
| 15 |
+
|
| 16 |
+
class OligomerClassifier(pl.LightningModule):
|
| 17 |
+
def __init__(self, config):
|
| 18 |
+
super().__init__()
|
| 19 |
+
self.config = config
|
| 20 |
+
self.loss_fn = nn.BCEWithLogitsLoss()
|
| 21 |
+
self.auroc = BinaryAUROC()
|
| 22 |
+
self.accuracy = BinaryAccuracy()
|
| 23 |
+
|
| 24 |
+
self.esm_model = AutoModel.from_pretrained(config.lm.pretrained_esm)
|
| 25 |
+
for p in self.esm_model.parameters():
|
| 26 |
+
p.requires_grad = False
|
| 27 |
+
|
| 28 |
+
self.down_proj = nn.Linear(config.model.esm_dim, config.model.d_model)
|
| 29 |
+
self.layer_norm = nn.LayerNorm(config.model.d_model)
|
| 30 |
+
self.dropout = nn.Dropout(config.model.dropout)
|
| 31 |
+
|
| 32 |
+
self.mlp = nn.Sequential(
|
| 33 |
+
nn.Linear(config.model.d_model, config.model.d_model // 2),
|
| 34 |
+
nn.ReLU(),
|
| 35 |
+
nn.Dropout(config.model.dropout),
|
| 36 |
+
nn.Linear(config.model.d_model // 2, 1),
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
def forward(self, batch):
|
| 40 |
+
attention_mask = batch["attention_mask"]
|
| 41 |
+
|
| 42 |
+
if "input_ids" in batch:
|
| 43 |
+
esm_embeds = self.get_esm_embeddings(batch["input_ids"], attention_mask)
|
| 44 |
+
else:
|
| 45 |
+
esm_embeds = batch["embeds"]
|
| 46 |
+
|
| 47 |
+
if esm_embeds.ndim == 2:
|
| 48 |
+
esm_embeds = esm_embeds.unsqueeze(0)
|
| 49 |
+
if attention_mask.ndim == 1:
|
| 50 |
+
attention_mask = attention_mask.unsqueeze(0)
|
| 51 |
+
|
| 52 |
+
x = self.down_proj(esm_embeds)
|
| 53 |
+
x = self.layer_norm(x)
|
| 54 |
+
x = self.dropout(x)
|
| 55 |
+
|
| 56 |
+
mask = attention_mask.unsqueeze(-1).float()
|
| 57 |
+
x = x * mask
|
| 58 |
+
|
| 59 |
+
pooled = x.sum(dim=1) / mask.sum(dim=1).clamp(min=1.0)
|
| 60 |
+
logits = self.mlp(pooled).squeeze(-1)
|
| 61 |
+
|
| 62 |
+
return logits
|
| 63 |
+
|
| 64 |
+
# -------# Training / Evaluation #-------- #
|
| 65 |
+
def training_step(self, batch, batch_idx):
|
| 66 |
+
train_loss, _ = self.compute_loss(batch)
|
| 67 |
+
self.log(name="train/loss", value=train_loss.item(), on_step=True, on_epoch=False, logger=True, sync_dist=True)
|
| 68 |
+
self.save_ckpt()
|
| 69 |
+
return train_loss
|
| 70 |
+
|
| 71 |
+
def validation_step(self, batch, batch_idx):
|
| 72 |
+
val_loss, _ = self.compute_loss(batch)
|
| 73 |
+
self.log(name="val/loss", value=val_loss.item(), on_step=False, on_epoch=True, logger=True, sync_dist=True)
|
| 74 |
+
return val_loss
|
| 75 |
+
|
| 76 |
+
def test_step(self, batch):
|
| 77 |
+
test_loss, preds = self.compute_loss(batch)
|
| 78 |
+
auroc, accuracy = self.get_metrics(batch, preds)
|
| 79 |
+
self.log(name="test/loss", value=test_loss.item(), on_step=False, on_epoch=True, logger=True, sync_dist=True)
|
| 80 |
+
self.log(name="test/AUROC", value=auroc.item(), on_step=False, on_epoch=True, logger=True, sync_dist=True)
|
| 81 |
+
self.log(name="test/accuracy", value=accuracy.item(), on_step=False, on_epoch=True, logger=True, sync_dist=True)
|
| 82 |
+
return test_loss
|
| 83 |
+
|
| 84 |
+
def on_test_epoch_end(self):
|
| 85 |
+
self.auroc.reset()
|
| 86 |
+
self.accuracy.reset()
|
| 87 |
+
|
| 88 |
+
def optimizer_step(self, *args, **kwargs):
|
| 89 |
+
super().optimizer_step(*args, **kwargs)
|
| 90 |
+
gc.collect()
|
| 91 |
+
torch.cuda.empty_cache()
|
| 92 |
+
|
| 93 |
+
def configure_optimizers(self):
|
| 94 |
+
path = self.config.training
|
| 95 |
+
optimizer = torch.optim.AdamW(self.parameters(), lr=self.config.optim.lr)
|
| 96 |
+
lr_scheduler = CosineWarmup(
|
| 97 |
+
optimizer,
|
| 98 |
+
warmup_steps=path.warmup_steps,
|
| 99 |
+
total_steps=path.max_steps,
|
| 100 |
+
)
|
| 101 |
+
scheduler_dict = {
|
| 102 |
+
"scheduler": lr_scheduler,
|
| 103 |
+
"interval": 'step',
|
| 104 |
+
'frequency': 1,
|
| 105 |
+
'monitor': 'val/loss',
|
| 106 |
+
'name': 'learning_rate'
|
| 107 |
+
}
|
| 108 |
+
return [optimizer], [scheduler_dict]
|
| 109 |
+
|
| 110 |
+
def save_ckpt(self):
|
| 111 |
+
curr_step = self.global_step
|
| 112 |
+
save_every = self.config.training.val_check_interval
|
| 113 |
+
if curr_step % save_every == 0 and curr_step > 0: # Save every 250 steps
|
| 114 |
+
ckpt_path = f"{self.config.checkpointing.save_dir}/step={curr_step}.ckpt"
|
| 115 |
+
self.trainer.save_checkpoint(ckpt_path)
|
| 116 |
+
|
| 117 |
+
# -------# Loss and Test Set Metrics #-------- #
|
| 118 |
+
@torch.no_grad
|
| 119 |
+
def get_esm_embeddings(self, input_ids, attention_mask):
|
| 120 |
+
outputs = self.esm_model(input_ids=input_ids, attention_mask=attention_mask)
|
| 121 |
+
embeddings = outputs.last_hidden_state
|
| 122 |
+
return embeddings
|
| 123 |
+
|
| 124 |
+
def compute_loss(self, batch):
|
| 125 |
+
"""Helper method to handle loss calculation"""
|
| 126 |
+
labels = batch['labels']
|
| 127 |
+
preds = self.forward(batch)
|
| 128 |
+
loss = self.loss_fn(preds, labels)
|
| 129 |
+
return loss, preds
|
| 130 |
+
|
| 131 |
+
def get_metrics(self, batch, preds):
|
| 132 |
+
"""Helper method to compute metrics"""
|
| 133 |
+
labels = batch['labels']
|
| 134 |
+
_print(f"labels {labels.shape}")
|
| 135 |
+
_print(f"preds {preds.shape}")
|
| 136 |
+
auroc = self.auroc.forward(preds, labels)
|
| 137 |
+
accuracy = self.accuracy.forward(preds, labels)
|
| 138 |
+
return auroc, accuracy
|
| 139 |
+
|
| 140 |
+
# -------# Helper Functions #-------- #
|
| 141 |
+
def get_state_dict(self, ckpt_path):
|
| 142 |
+
"""Helper method to load and process a trained model's state dict from saved checkpoint"""
|
| 143 |
+
def remove_model_prefix(state_dict):
|
| 144 |
+
for k in state_dict.keys():
|
| 145 |
+
if "model." in k:
|
| 146 |
+
k.replace('model.', '')
|
| 147 |
+
return state_dict
|
| 148 |
+
|
| 149 |
+
checkpoint = torch.load(ckpt_path, weights_only=False)#, map_location='cuda' if torch.cuda.is_available() else 'cpu')
|
| 150 |
+
state_dict = checkpoint.get("state_dict", checkpoint)
|
| 151 |
+
|
| 152 |
+
if any(k.startswith("model.") for k in state_dict.keys()):
|
| 153 |
+
state_dict = remove_model_prefix(state_dict)
|
| 154 |
+
|
| 155 |
+
return state_dict
|
src/guidance/oligo/utils.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import numpy as np
|
| 5 |
+
from torch.optim.lr_scheduler import _LRScheduler
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class HelixRoPE(nn.Module):
|
| 10 |
+
def __init__(self, config):
|
| 11 |
+
super().__init__()
|
| 12 |
+
pos = torch.arange(config.data.max_seq_len).float()
|
| 13 |
+
thetas = (2 * torch.pi / 3.6) * pos
|
| 14 |
+
self.register_buffer("thetas", thetas)
|
| 15 |
+
|
| 16 |
+
def forward(self, x, mask):
|
| 17 |
+
B, L, D = x.shape
|
| 18 |
+
assert D % 2 == 0
|
| 19 |
+
|
| 20 |
+
thetas = self.thetas[:L]
|
| 21 |
+
cos = torch.cos(thetas).unsqueeze(0).unsqueeze(-1)
|
| 22 |
+
sin = torch.sin(thetas).unsqueeze(0).unsqueeze(-1)
|
| 23 |
+
|
| 24 |
+
x_double = x.view(B, L, D//2, 2)
|
| 25 |
+
x1 = x_double[..., 0]
|
| 26 |
+
x2 = x_double[..., 1]
|
| 27 |
+
|
| 28 |
+
r1 = cos * x1 - sin * x2
|
| 29 |
+
r2 = sin * x1 + cos * x2
|
| 30 |
+
|
| 31 |
+
ropes = torch.stack([r1, r2], dim=-1).view(B, L, D)
|
| 32 |
+
return ropes * mask # attention mask to ignore pad tokens
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class CosineWarmup(_LRScheduler):
|
| 36 |
+
def __init__(self, optimizer, warmup_steps, total_steps, eta_ratio=0.1, last_epoch=-1):
|
| 37 |
+
self.warmup_steps = warmup_steps
|
| 38 |
+
self.total_steps = total_steps
|
| 39 |
+
self.eta_ratio = eta_ratio # The ratio of minimum to maximum learning rate
|
| 40 |
+
super(CosineWarmup, self).__init__(optimizer, last_epoch)
|
| 41 |
+
|
| 42 |
+
def get_lr(self):
|
| 43 |
+
if self.last_epoch < self.warmup_steps:
|
| 44 |
+
return [base_lr * self.last_epoch / self.warmup_steps for base_lr in self.base_lrs]
|
| 45 |
+
|
| 46 |
+
progress = (self.last_epoch - self.warmup_steps) / (self.total_steps - self.warmup_steps)
|
| 47 |
+
cosine_decay = 0.5 * (1 + np.cos(np.pi * progress))
|
| 48 |
+
decayed_lr = (1 - self.eta_ratio) * cosine_decay + self.eta_ratio
|
| 49 |
+
|
| 50 |
+
return [decayed_lr * base_lr for base_lr in self.base_lrs]
|
src/guidance/{dataloader.py → solubility/dataloader.py}
RENAMED
|
File without changes
|
src/guidance/{main.py → solubility/main.py}
RENAMED
|
@@ -10,12 +10,13 @@ from lightning.pytorch.loggers import WandbLogger
|
|
| 10 |
from lightning.pytorch.callbacks import ModelCheckpoint, LearningRateMonitor
|
| 11 |
|
| 12 |
from src.utils.model_utils import _print
|
| 13 |
-
from src.
|
| 14 |
-
from src.guidance.
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
-
config =
|
| 18 |
-
wandb.login(
|
| 19 |
|
| 20 |
# data
|
| 21 |
datasets = get_datasets(config)
|
|
|
|
| 10 |
from lightning.pytorch.callbacks import ModelCheckpoint, LearningRateMonitor
|
| 11 |
|
| 12 |
from src.utils.model_utils import _print
|
| 13 |
+
from src.utils.config_utils import load_config
|
| 14 |
+
from src.guidance.solubility.solubility_module import SolubilityClassifier
|
| 15 |
+
from src.guidance.solubility.dataloader import MembraneDataModule, get_datasets
|
| 16 |
|
| 17 |
|
| 18 |
+
config = load_config("solubility.yaml")
|
| 19 |
+
wandb.login()
|
| 20 |
|
| 21 |
# data
|
| 22 |
datasets = get_datasets(config)
|
src/guidance/{solubility_module.py → solubility/solubility_module.py}
RENAMED
|
@@ -8,10 +8,10 @@ from transformers import AutoModel
|
|
| 8 |
from torchmetrics.classification import BinaryAUROC, BinaryAccuracy
|
| 9 |
|
| 10 |
from src.utils.model_utils import _print
|
| 11 |
-
from src.guidance.utils import CosineWarmup
|
| 12 |
|
| 13 |
|
| 14 |
-
config = OmegaConf.load("/scratch/sgoel/MeMDLM_v2/src/configs/
|
| 15 |
|
| 16 |
class SolubilityClassifier(pl.LightningModule):
|
| 17 |
def __init__(self, config):
|
|
@@ -146,7 +146,7 @@ class SolubilityClassifier(pl.LightningModule):
|
|
| 146 |
k.replace('model.', '')
|
| 147 |
return state_dict
|
| 148 |
|
| 149 |
-
checkpoint = torch.load(ckpt_path, map_location='cuda' if torch.cuda.is_available() else 'cpu')
|
| 150 |
state_dict = checkpoint.get("state_dict", checkpoint)
|
| 151 |
|
| 152 |
if any(k.startswith("model.") for k in state_dict.keys()):
|
|
|
|
| 8 |
from torchmetrics.classification import BinaryAUROC, BinaryAccuracy
|
| 9 |
|
| 10 |
from src.utils.model_utils import _print
|
| 11 |
+
from src.guidance.solubility.utils import CosineWarmup
|
| 12 |
|
| 13 |
|
| 14 |
+
config = OmegaConf.load("/scratch/pranamlab/sgoel/MeMDLM_v2/src/configs/solubility.yaml")
|
| 15 |
|
| 16 |
class SolubilityClassifier(pl.LightningModule):
|
| 17 |
def __init__(self, config):
|
|
|
|
| 146 |
k.replace('model.', '')
|
| 147 |
return state_dict
|
| 148 |
|
| 149 |
+
checkpoint = torch.load(ckpt_path, weights_only=False)#, map_location='cuda' if torch.cuda.is_available() else 'cpu')
|
| 150 |
state_dict = checkpoint.get("state_dict", checkpoint)
|
| 151 |
|
| 152 |
if any(k.startswith("model.") for k in state_dict.keys()):
|
src/guidance/solubility/utils.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
from torch.optim.lr_scheduler import _LRScheduler
|
| 5 |
+
|
| 6 |
+
class CosineWarmup(_LRScheduler):
|
| 7 |
+
def __init__(self, optimizer, warmup_steps, total_steps, eta_ratio=0.1, last_epoch=-1):
|
| 8 |
+
self.warmup_steps = warmup_steps
|
| 9 |
+
self.total_steps = total_steps
|
| 10 |
+
self.eta_ratio = eta_ratio # The ratio of minimum to maximum learning rate
|
| 11 |
+
super(CosineWarmup, self).__init__(optimizer, last_epoch)
|
| 12 |
+
|
| 13 |
+
def get_lr(self):
|
| 14 |
+
if self.last_epoch < self.warmup_steps:
|
| 15 |
+
return [base_lr * self.last_epoch / self.warmup_steps for base_lr in self.base_lrs]
|
| 16 |
+
|
| 17 |
+
progress = (self.last_epoch - self.warmup_steps) / (self.total_steps - self.warmup_steps)
|
| 18 |
+
cosine_decay = 0.5 * (1 + np.cos(np.pi * progress))
|
| 19 |
+
decayed_lr = (1 - self.eta_ratio) * cosine_decay + self.eta_ratio
|
| 20 |
+
|
| 21 |
+
return [decayed_lr * base_lr for base_lr in self.base_lrs]
|
src/lm/memdlm/diffusion_module.py
CHANGED
|
@@ -28,7 +28,7 @@ class MembraneDiffusion(pl.LightningModule):
|
|
| 28 |
self.mask_id = self.tokenizer.mask_token_id
|
| 29 |
self.pad_id = self.tokenizer.pad_token_id
|
| 30 |
|
| 31 |
-
def forward(self, input_ids, attention_mask,
|
| 32 |
"""
|
| 33 |
Forward pass through language model.
|
| 34 |
|
|
@@ -38,7 +38,12 @@ class MembraneDiffusion(pl.LightningModule):
|
|
| 38 |
Returns:
|
| 39 |
- logits (torch.Tensor): [B, L, V], unnormalized model outputs
|
| 40 |
"""
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
# -------# Diffusion #-------- #
|
| 44 |
def step(self, batch):
|
|
@@ -78,7 +83,6 @@ class MembraneDiffusion(pl.LightningModule):
|
|
| 78 |
u = torch.rand_like(x0, dtype=torch.float)
|
| 79 |
t1_mask = (u < (t1 / self.config.lm.num_diffusion_timesteps)[:, None]) & maskable_mask
|
| 80 |
x_t1 = x0.masked_fill(t1_mask, self.mask_id)
|
| 81 |
-
x_t1 = x_t1.masked_fill(t1_mask, self.mask_id)
|
| 82 |
return x_t1, t1_mask
|
| 83 |
|
| 84 |
def get_weight(self, t, weight_type):
|
|
@@ -100,7 +104,7 @@ class MembraneDiffusion(pl.LightningModule):
|
|
| 100 |
|
| 101 |
Args:
|
| 102 |
logits (torch.Tensor): [B, L, vocab_size], unnormalized model outputs
|
| 103 |
-
labels (torch.Tensor): [B, L], target labels
|
| 104 |
weight (torch.Tensor): [B, 1], per-sample weight for loss calculation
|
| 105 |
Returns:
|
| 106 |
loss (torch.Tensor): Averaged loss over the batch
|
|
@@ -127,22 +131,22 @@ class MembraneDiffusion(pl.LightningModule):
|
|
| 127 |
# -------# Training / Evaluation #-------- #
|
| 128 |
def training_step(self, batch):
|
| 129 |
loss, ppl = self.step(batch)
|
| 130 |
-
self.log("train/loss", loss
|
| 131 |
-
self.log("train/ppl", ppl
|
| 132 |
return loss
|
| 133 |
|
| 134 |
def validation_step(self, batch):
|
| 135 |
loss, ppl = self.step(batch)
|
| 136 |
self.cleanup()
|
| 137 |
-
self.log("val/loss", loss
|
| 138 |
-
self.log("val/ppl", ppl
|
| 139 |
return loss
|
| 140 |
|
| 141 |
def test_step(self, batch):
|
| 142 |
loss, ppl = self.step(batch)
|
| 143 |
self.cleanup()
|
| 144 |
-
self.log('test/loss', loss
|
| 145 |
-
self.log("test/ppl", ppl
|
| 146 |
return loss
|
| 147 |
|
| 148 |
|
|
@@ -176,7 +180,7 @@ class MembraneDiffusion(pl.LightningModule):
|
|
| 176 |
k.replace('model.', '')
|
| 177 |
return state_dict
|
| 178 |
|
| 179 |
-
checkpoint = torch.load(ckpt_path, map_location='cuda' if torch.cuda.is_available() else 'cpu')
|
| 180 |
state_dict = checkpoint.get("state_dict", checkpoint)
|
| 181 |
|
| 182 |
if any(k.startswith("model.") for k in state_dict.keys()):
|
|
|
|
| 28 |
self.mask_id = self.tokenizer.mask_token_id
|
| 29 |
self.pad_id = self.tokenizer.pad_token_id
|
| 30 |
|
| 31 |
+
def forward(self, input_ids, attention_mask, return_hidden=None):
|
| 32 |
"""
|
| 33 |
Forward pass through language model.
|
| 34 |
|
|
|
|
| 38 |
Returns:
|
| 39 |
- logits (torch.Tensor): [B, L, V], unnormalized model outputs
|
| 40 |
"""
|
| 41 |
+
if return_hidden:
|
| 42 |
+
out = self.model(input_ids=input_ids, attention_mask=attention_mask, output_hidden_states=True)
|
| 43 |
+
return out.hidden_states[-1]
|
| 44 |
+
|
| 45 |
+
else:
|
| 46 |
+
return self.model(input_ids=input_ids, attention_mask=attention_mask).logits
|
| 47 |
|
| 48 |
# -------# Diffusion #-------- #
|
| 49 |
def step(self, batch):
|
|
|
|
| 83 |
u = torch.rand_like(x0, dtype=torch.float)
|
| 84 |
t1_mask = (u < (t1 / self.config.lm.num_diffusion_timesteps)[:, None]) & maskable_mask
|
| 85 |
x_t1 = x0.masked_fill(t1_mask, self.mask_id)
|
|
|
|
| 86 |
return x_t1, t1_mask
|
| 87 |
|
| 88 |
def get_weight(self, t, weight_type):
|
|
|
|
| 104 |
|
| 105 |
Args:
|
| 106 |
logits (torch.Tensor): [B, L, vocab_size], unnormalized model outputs
|
| 107 |
+
labels (torch.Tensor): [B, L], target labels with pad tokens
|
| 108 |
weight (torch.Tensor): [B, 1], per-sample weight for loss calculation
|
| 109 |
Returns:
|
| 110 |
loss (torch.Tensor): Averaged loss over the batch
|
|
|
|
| 131 |
# -------# Training / Evaluation #-------- #
|
| 132 |
def training_step(self, batch):
|
| 133 |
loss, ppl = self.step(batch)
|
| 134 |
+
self.log("train/loss", loss, on_step=True, on_epoch=False, prog_bar=True)
|
| 135 |
+
self.log("train/ppl", ppl, on_step=True, on_epoch=False, prog_bar=False)
|
| 136 |
return loss
|
| 137 |
|
| 138 |
def validation_step(self, batch):
|
| 139 |
loss, ppl = self.step(batch)
|
| 140 |
self.cleanup()
|
| 141 |
+
self.log("val/loss", loss, on_step=False, on_epoch=True, prog_bar=True, sync_dist=True)
|
| 142 |
+
self.log("val/ppl", ppl, on_step=False, on_epoch=True, prog_bar=False, sync_dist=True)
|
| 143 |
return loss
|
| 144 |
|
| 145 |
def test_step(self, batch):
|
| 146 |
loss, ppl = self.step(batch)
|
| 147 |
self.cleanup()
|
| 148 |
+
self.log('test/loss', loss, on_step=False, on_epoch=True, prog_bar=True, sync_dist=True)
|
| 149 |
+
self.log("test/ppl", ppl, on_step=False, on_epoch=True, prog_bar=False, sync_dist=True)
|
| 150 |
return loss
|
| 151 |
|
| 152 |
|
|
|
|
| 180 |
k.replace('model.', '')
|
| 181 |
return state_dict
|
| 182 |
|
| 183 |
+
checkpoint = torch.load(ckpt_path, weights_only=False)#, map_location='cuda' if torch.cuda.is_available() else 'cpu')
|
| 184 |
state_dict = checkpoint.get("state_dict", checkpoint)
|
| 185 |
|
| 186 |
if any(k.startswith("model.") for k in state_dict.keys()):
|
src/lm/memdlm/main.py
CHANGED
|
@@ -14,12 +14,13 @@ from lightning.pytorch.callbacks import ModelCheckpoint, LearningRateMonitor
|
|
| 14 |
from src.lm.memdlm.diffusion_module import MembraneDiffusion
|
| 15 |
from src.lm.memdlm.dataloader import MembraneDataModule, get_datasets
|
| 16 |
from src.utils.model_utils import apply_rdm_freezing
|
|
|
|
| 17 |
|
| 18 |
-
wandb.login(
|
| 19 |
|
| 20 |
|
| 21 |
# Load yaml config
|
| 22 |
-
config =
|
| 23 |
|
| 24 |
# Get datasets
|
| 25 |
datasets = get_datasets(config)
|
|
|
|
| 14 |
from src.lm.memdlm.diffusion_module import MembraneDiffusion
|
| 15 |
from src.lm.memdlm.dataloader import MembraneDataModule, get_datasets
|
| 16 |
from src.utils.model_utils import apply_rdm_freezing
|
| 17 |
+
from src.utils.config_utils import load_config
|
| 18 |
|
| 19 |
+
wandb.login()
|
| 20 |
|
| 21 |
|
| 22 |
# Load yaml config
|
| 23 |
+
config = load_config("lm.yaml")
|
| 24 |
|
| 25 |
# Get datasets
|
| 26 |
datasets = get_datasets(config)
|
src/sampling/{guided_generator.py → desolubilize_generator.py}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
|
| 3 |
-
|
| 4 |
import os
|
| 5 |
import torch
|
| 6 |
import pandas as pd
|
|
@@ -9,20 +9,30 @@ from datetime import datetime
|
|
| 9 |
from omegaconf import OmegaConf
|
| 10 |
from transformers import AutoTokenizer, AutoModelForMaskedLM
|
| 11 |
|
| 12 |
-
from src.lm.memdlm.diffusion_module import
|
| 13 |
-
from src.
|
| 14 |
-
from src.sampling.guided_sampler import GuidedSampler
|
| 15 |
from src.utils.generate_utils import (
|
| 16 |
mask_for_scaffold,
|
| 17 |
calc_blosum_score,
|
| 18 |
-
calc_ppl
|
|
|
|
| 19 |
)
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
|
| 24 |
todays_date = datetime.today().strftime('%Y-%m-%d')
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
try: os.makedirs(csv_save_path, exist_ok=False)
|
| 27 |
except FileExistsError: pass
|
| 28 |
|
|
@@ -33,25 +43,28 @@ def main():
|
|
| 33 |
tokenizer = AutoTokenizer.from_pretrained(config.lm.pretrained_esm)
|
| 34 |
esm_model = AutoModelForMaskedLM.from_pretrained(config.lm.pretrained_esm).eval().to(device)
|
| 35 |
|
| 36 |
-
diffusion =
|
| 37 |
-
state_dict = diffusion.get_state_dict(
|
| 38 |
diffusion.load_state_dict(state_dict)
|
| 39 |
diffusion.eval().to(device)
|
| 40 |
|
| 41 |
-
sampler =
|
| 42 |
|
| 43 |
-
|
|
|
|
| 44 |
sequences = df['Sequence'].tolist()
|
| 45 |
|
| 46 |
-
gen_seqs, ppls, blosums = [], [], []
|
| 47 |
|
| 48 |
|
| 49 |
-
for seq in tqdm(sequences, desc='
|
| 50 |
masked_seq = mask_for_scaffold(seq, generate_type='uppercase', mask_token='<mask>')
|
| 51 |
tokens = tokenizer(masked_seq, return_tensors='pt')
|
| 52 |
input_ids, attn_masks = tokens['input_ids'].to(device), tokens['attention_mask'].to(device)
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
| 55 |
infilled_tokens = sampler.optimize_sequence(
|
| 56 |
input_ids=input_ids,
|
| 57 |
attn_masks=attn_masks,
|
|
@@ -59,32 +72,49 @@ def main():
|
|
| 59 |
)
|
| 60 |
infilled_seq = tokenizer.decode(infilled_tokens).replace(" ", "")[5:-5]
|
| 61 |
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
try:
|
| 64 |
ppl = calc_ppl(esm_model, tokenizer, infilled_seq, [i for i in range(len(seq))], model_type='esm')
|
| 65 |
except:
|
| 66 |
ppl = float('inf')
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
gen_seqs.append(infilled_seq)
|
| 69 |
ppls.append(ppl)
|
| 70 |
blosums.append(bl)
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
_print(seq)
|
| 73 |
_print(infilled_seq)
|
| 74 |
_print(ppl)
|
| 75 |
_print(bl)
|
|
|
|
|
|
|
|
|
|
| 76 |
_print('\n')
|
| 77 |
|
| 78 |
|
| 79 |
df['MeMDLM Sequence'] = gen_seqs
|
| 80 |
df['MeMDLM PPL'] = ppls
|
| 81 |
df['MeMDLM BLOSUM'] = blosums
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
_print(df)
|
| 84 |
-
df.to_csv(
|
| 85 |
|
| 86 |
|
| 87 |
|
| 88 |
if __name__ == "__main__":
|
| 89 |
main()
|
| 90 |
-
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
|
| 3 |
+
|
| 4 |
import os
|
| 5 |
import torch
|
| 6 |
import pandas as pd
|
|
|
|
| 9 |
from omegaconf import OmegaConf
|
| 10 |
from transformers import AutoTokenizer, AutoModelForMaskedLM
|
| 11 |
|
| 12 |
+
from src.lm.memdlm.diffusion_module import MembraneDiffusion
|
| 13 |
+
from src.sampling.pet_sampler import PETSampler
|
|
|
|
| 14 |
from src.utils.generate_utils import (
|
| 15 |
mask_for_scaffold,
|
| 16 |
calc_blosum_score,
|
| 17 |
+
calc_ppl,
|
| 18 |
+
calc_tm_enrich
|
| 19 |
)
|
| 20 |
|
| 21 |
+
from src.utils.model_utils import _print
|
| 22 |
+
from src.utils.config_utils import load_config, repo_path
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
config = load_config("desolubilize.yaml")
|
| 26 |
+
task = config.guidance.task
|
| 27 |
|
| 28 |
+
results_root = repo_path("results", "heme", config.lm.ft_evoflow)
|
| 29 |
todays_date = datetime.today().strftime('%Y-%m-%d')
|
| 30 |
+
|
| 31 |
+
if config.guidance.prior == 'boltzmann':
|
| 32 |
+
csv_save_path = results_root / task / todays_date / f"{config.guidance.prior}-t={config.guidance.boltzmann_t}_p={config.guidance.top_p}_tau={config.guidance.sampling_t}"
|
| 33 |
+
elif config.guidance.prior == 'lm_probs':
|
| 34 |
+
csv_save_path = results_root / task / todays_date / f"{config.guidance.prior}_p={config.guidance.top_p}_tau={config.guidance.sampling_t}"
|
| 35 |
+
|
| 36 |
try: os.makedirs(csv_save_path, exist_ok=False)
|
| 37 |
except FileExistsError: pass
|
| 38 |
|
|
|
|
| 43 |
tokenizer = AutoTokenizer.from_pretrained(config.lm.pretrained_esm)
|
| 44 |
esm_model = AutoModelForMaskedLM.from_pretrained(config.lm.pretrained_esm).eval().to(device)
|
| 45 |
|
| 46 |
+
diffusion = MembraneDiffusion(config).to(device)
|
| 47 |
+
state_dict = diffusion.get_state_dict(str(repo_path("checkpoints", config.lm.ft_evoflow, "best_model.ckpt")))
|
| 48 |
diffusion.load_state_dict(state_dict)
|
| 49 |
diffusion.eval().to(device)
|
| 50 |
|
| 51 |
+
sampler = PETSampler(config, esm_model, tokenizer, diffusion, device)
|
| 52 |
|
| 53 |
+
# Update this path to your input CSV of scaffold sequences (uppercase = TM, lowercase = soluble).
|
| 54 |
+
df = pd.read_csv(str(repo_path("results", "heme", "4d2.csv")))
|
| 55 |
sequences = df['Sequence'].tolist()
|
| 56 |
|
| 57 |
+
gen_seqs, ppls, blosums, og_tms, gen_tms, delta_tms = [], [], [], [], [], []
|
| 58 |
|
| 59 |
|
| 60 |
+
for seq in tqdm(sequences, desc='Desolubilizing Sequences'):
|
| 61 |
masked_seq = mask_for_scaffold(seq, generate_type='uppercase', mask_token='<mask>')
|
| 62 |
tokens = tokenizer(masked_seq, return_tensors='pt')
|
| 63 |
input_ids, attn_masks = tokens['input_ids'].to(device), tokens['attention_mask'].to(device)
|
| 64 |
+
|
| 65 |
+
tm_idxs = [i for i in range(len(seq)) if seq[i].isupper()]
|
| 66 |
+
soluble_idxs = [i + 1 for i in range(len(seq)) if seq[i].islower()]
|
| 67 |
+
|
| 68 |
infilled_tokens = sampler.optimize_sequence(
|
| 69 |
input_ids=input_ids,
|
| 70 |
attn_masks=attn_masks,
|
|
|
|
| 72 |
)
|
| 73 |
infilled_seq = tokenizer.decode(infilled_tokens).replace(" ", "")[5:-5]
|
| 74 |
|
| 75 |
+
try:
|
| 76 |
+
bl = calc_blosum_score(seq.upper(), infilled_seq, tm_idxs)
|
| 77 |
+
except:
|
| 78 |
+
bl = float('inf')
|
| 79 |
+
|
| 80 |
try:
|
| 81 |
ppl = calc_ppl(esm_model, tokenizer, infilled_seq, [i for i in range(len(seq))], model_type='esm')
|
| 82 |
except:
|
| 83 |
ppl = float('inf')
|
| 84 |
|
| 85 |
+
try:
|
| 86 |
+
og_tm, gen_tm, delta_tm = calc_tm_enrich(seq.upper(), infilled_seq, tm_idxs)
|
| 87 |
+
except:
|
| 88 |
+
og_tm, gen_tm, delta_tm = float('inf'), float('inf'), float('inf')
|
| 89 |
+
|
| 90 |
gen_seqs.append(infilled_seq)
|
| 91 |
ppls.append(ppl)
|
| 92 |
blosums.append(bl)
|
| 93 |
+
og_tms.append(og_tm)
|
| 94 |
+
gen_tms.append(gen_tm)
|
| 95 |
+
delta_tms.append(delta_tm)
|
| 96 |
|
| 97 |
_print(seq)
|
| 98 |
_print(infilled_seq)
|
| 99 |
_print(ppl)
|
| 100 |
_print(bl)
|
| 101 |
+
_print(og_tm)
|
| 102 |
+
_print(gen_tm)
|
| 103 |
+
_print(delta_tm)
|
| 104 |
_print('\n')
|
| 105 |
|
| 106 |
|
| 107 |
df['MeMDLM Sequence'] = gen_seqs
|
| 108 |
df['MeMDLM PPL'] = ppls
|
| 109 |
df['MeMDLM BLOSUM'] = blosums
|
| 110 |
+
df['OG TM Enrichment'] = og_tms
|
| 111 |
+
df['MeMDLM TM Enrichment'] = gen_tms
|
| 112 |
+
df['Delta TM Enrichment'] = delta_tms
|
| 113 |
|
| 114 |
_print(df)
|
| 115 |
+
df.to_csv(csv_save_path / "infilled_seqs.csv", index=False)
|
| 116 |
|
| 117 |
|
| 118 |
|
| 119 |
if __name__ == "__main__":
|
| 120 |
main()
|
|
|
src/sampling/multipass_generator.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
import random
|
| 7 |
+
import torch
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
from collections import Counter
|
| 13 |
+
from omegaconf import OmegaConf
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
from src.utils.generate_utils import mask_for_de_novo
|
| 16 |
+
from transformers import AutoTokenizer, AutoModelForMaskedLM
|
| 17 |
+
|
| 18 |
+
from src.lm.memdlm.diffusion_module import MembraneDiffusion
|
| 19 |
+
from src.sampling.multipass_sampler import MultipassSampler
|
| 20 |
+
|
| 21 |
+
from src.utils.generate_utils import calc_ppl
|
| 22 |
+
from src.utils.model_utils import _print
|
| 23 |
+
from src.utils.config_utils import load_config, repo_path
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 27 |
+
config = load_config("multipass.yaml")
|
| 28 |
+
|
| 29 |
+
date = datetime.now().strftime("%Y-%m-%d")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def main():
|
| 35 |
+
csv_save_path = repo_path(
|
| 36 |
+
'results', 'multipass', config.wandb.name, date,
|
| 37 |
+
f"lamb={config.guidance.reg_strength}_tau={config.guidance.sampling_temperature}"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
try: os.makedirs(csv_save_path, exist_ok=False)
|
| 41 |
+
except FileExistsError: pass
|
| 42 |
+
|
| 43 |
+
tokenizer = AutoTokenizer.from_pretrained(config.lm.pretrained_evoflow)
|
| 44 |
+
|
| 45 |
+
memdlm = MembraneDiffusion(config).to(device)
|
| 46 |
+
state_dict = memdlm.get_state_dict(str(repo_path("checkpoints", config.lm.ft_evoflow, "best_model.ckpt")))
|
| 47 |
+
memdlm.load_state_dict(state_dict)
|
| 48 |
+
memdlm.eval()
|
| 49 |
+
|
| 50 |
+
esm_pth = config.lm.pretrained_esm
|
| 51 |
+
esm_model = AutoModelForMaskedLM.from_pretrained(esm_pth).to(device)
|
| 52 |
+
esm_model.eval()
|
| 53 |
+
|
| 54 |
+
generator = MultipassSampler(config, device, memdlm, tokenizer)
|
| 55 |
+
|
| 56 |
+
seq_lengths = [x for x in range(60, 161) for _ in range(3)]
|
| 57 |
+
#seq_lengths = random.sample([x for x in range(120, 161)], 15)
|
| 58 |
+
|
| 59 |
+
generation_results = []
|
| 60 |
+
for seq_len in tqdm(seq_lengths, desc=f"Generating sequences: "):
|
| 61 |
+
seq_res = []
|
| 62 |
+
|
| 63 |
+
masked_seq = mask_for_de_novo(seq_len) # Sequence of all <mask> tokens
|
| 64 |
+
tokens = tokenizer(masked_seq, return_tensors='pt')
|
| 65 |
+
|
| 66 |
+
gen_seq = ""
|
| 67 |
+
attempts = 0
|
| 68 |
+
|
| 69 |
+
while len(gen_seq) != seq_len and attempts < 3:
|
| 70 |
+
gen_seq, pred_tm_segs = generator.sample_guidance(
|
| 71 |
+
tokens,
|
| 72 |
+
config.guidance.guide_steps,
|
| 73 |
+
config.guidance.diffusion_steps
|
| 74 |
+
)
|
| 75 |
+
attempts += 1
|
| 76 |
+
|
| 77 |
+
if len(gen_seq) != seq_len:
|
| 78 |
+
esm_ppl, memdlm_ppl = None, None
|
| 79 |
+
else:
|
| 80 |
+
esm_ppl = calc_ppl(esm_model, tokenizer, gen_seq, [i for i in range(len(gen_seq))], model_type='esm')
|
| 81 |
+
memdlm_ppl = calc_ppl(memdlm, tokenizer, gen_seq, [i for i in range(len(gen_seq))], model_type='diffusion')
|
| 82 |
+
|
| 83 |
+
_print(f'seq: {gen_seq}')
|
| 84 |
+
_print(f'pred_tm_segs: {pred_tm_segs}')
|
| 85 |
+
_print(f"ESM PPL: {esm_ppl}")
|
| 86 |
+
_print(f"MeMDLM PPL: {memdlm_ppl}")
|
| 87 |
+
_print('\n')
|
| 88 |
+
|
| 89 |
+
seq_res.append(gen_seq)
|
| 90 |
+
seq_res.append(esm_ppl)
|
| 91 |
+
seq_res.append(memdlm_ppl)
|
| 92 |
+
seq_res.append(pred_tm_segs)
|
| 93 |
+
generation_results.append(seq_res)
|
| 94 |
+
|
| 95 |
+
df = pd.DataFrame(generation_results, columns=['Generated Sequence', 'ESM PPL', 'MeMDLM PPL', 'Pred TM Segments'])
|
| 96 |
+
df.to_csv(str(csv_save_path / "seqs_with_ppl.csv"), index=False)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
if __name__ == "__main__":
|
| 100 |
+
main()
|
src/sampling/multipass_sampler.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
from torch import nn
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
|
| 6 |
+
from src.guidance.multipass.multipass_module import MultipassClassifier
|
| 7 |
+
from src.utils.model_utils import _print
|
| 8 |
+
from src.utils.config_utils import repo_path
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class MultipassSampler:
|
| 13 |
+
def __init__(self, config, device, mdlm, tokenizer):
|
| 14 |
+
self.config = config
|
| 15 |
+
self.device = device
|
| 16 |
+
|
| 17 |
+
self.diffusion = mdlm
|
| 18 |
+
self.memdlm_lm = self.diffusion.model.lm_head
|
| 19 |
+
self.tokenizer = self.diffusion.tokenizer
|
| 20 |
+
|
| 21 |
+
ckpt_path = str(repo_path("checkpoints", config.wandb.name, "best_model.ckpt"))
|
| 22 |
+
self.classifier_model = MultipassClassifier(config, self.diffusion).eval().to(self.device)
|
| 23 |
+
state_dict = self.classifier_model.get_state_dict(ckpt_path)
|
| 24 |
+
self.classifier_model.load_state_dict(state_dict)
|
| 25 |
+
|
| 26 |
+
self.SPECIAL_TOKEN_IDS = {0, 1, 2, 3, 29}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def stochastic_sample_from_categorical(self, logits, temperature, noise_scale=1.0):
|
| 30 |
+
"""
|
| 31 |
+
Sample from a categorical distribution with optional temperature scaling and Gumbel noise.
|
| 32 |
+
Returns the sampled tokens and their log-probabilities (used as confidence scores).
|
| 33 |
+
"""
|
| 34 |
+
logits = logits.double()
|
| 35 |
+
if temperature != 0:
|
| 36 |
+
gumbel_noise = -torch.log(-torch.log(torch.rand_like(logits) + 1e-8) + 1e-8)
|
| 37 |
+
logits = (logits / temperature) + (noise_scale * gumbel_noise)
|
| 38 |
+
scores, tokens = logits.log_softmax(dim=-1).max(dim=-1)
|
| 39 |
+
return tokens, scores
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def topk_lowest_masking(self, scores, cutoff_len):
|
| 43 |
+
"""
|
| 44 |
+
scores: [b, n]; cutoff_len: [b, 1]
|
| 45 |
+
Returns a [b, n] bool mask, True at the cutoff_len lowest-scoring positions.
|
| 46 |
+
"""
|
| 47 |
+
sorted_index = scores.sort(-1)[0]
|
| 48 |
+
cutoff = sorted_index.gather(dim=-1, index=cutoff_len)
|
| 49 |
+
return scores < cutoff
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def classifier_score(self, hidden_states, attention_mask):
|
| 53 |
+
return self.classifier_model(x_t=None, attn_mask=attention_mask, with_hidden=True, embeds=hidden_states)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def guidance_loss(self, og_hidden, og_logits, attention_mask, delta):
|
| 57 |
+
"""
|
| 58 |
+
Implementation of explore-exploi guidance as in LaMBO-2 (https://arxiv.org/pdf/2305.20009).
|
| 59 |
+
Disregarding use of saliency map as we have one score per sequence.
|
| 60 |
+
"""
|
| 61 |
+
lamb = self.config.guidance.reg_strength
|
| 62 |
+
|
| 63 |
+
h_current = og_hidden + delta
|
| 64 |
+
new_logits = self.memdlm_lm(h_current)
|
| 65 |
+
score = self.classifier_score(h_current, attention_mask)
|
| 66 |
+
|
| 67 |
+
kl = F.kl_div(
|
| 68 |
+
F.log_softmax(new_logits, dim=-1),
|
| 69 |
+
F.softmax(og_logits, dim=-1),
|
| 70 |
+
reduction='sum'
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
loss = lamb * kl - score.sum()
|
| 74 |
+
return loss
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def optimized_sampling(self, og_logits, og_hidden, attention_mask, n_steps):
|
| 78 |
+
"""
|
| 79 |
+
At each diffusion timestep, take n_steps of gradient-based guidance on the hidden states
|
| 80 |
+
"""
|
| 81 |
+
eta = self.config.guidance.step_size
|
| 82 |
+
og_logits = og_logits.detach()
|
| 83 |
+
|
| 84 |
+
delta = nn.Parameter(torch.zeros_like(og_hidden), requires_grad=True)
|
| 85 |
+
optimizer = torch.optim.Adagrad([delta], lr=eta)
|
| 86 |
+
|
| 87 |
+
with torch.enable_grad():
|
| 88 |
+
for _ in range(n_steps):
|
| 89 |
+
optimizer.zero_grad()
|
| 90 |
+
loss = self.guidance_loss(og_hidden, og_logits, attention_mask, delta)
|
| 91 |
+
loss.backward()
|
| 92 |
+
optimizer.step()
|
| 93 |
+
|
| 94 |
+
h_new = og_hidden + delta.data
|
| 95 |
+
new_logits = self.memdlm_lm(h_new)
|
| 96 |
+
return new_logits, h_new
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def guided_logits(self, xt, attention_mask, guide_steps):
|
| 100 |
+
"""
|
| 101 |
+
Run the diffusion model on the current sequence and apply classifier guidance to the
|
| 102 |
+
hidden states, returning the guided LM-head logits used for scoring/sampling this step.
|
| 103 |
+
"""
|
| 104 |
+
with torch.no_grad():
|
| 105 |
+
hidden_states = self.diffusion(xt, attention_mask, return_hidden=True)
|
| 106 |
+
hidden_states = hidden_states.unsqueeze(0) if hidden_states.ndim != 3 else hidden_states
|
| 107 |
+
base_logits = self.memdlm_lm(hidden_states)
|
| 108 |
+
|
| 109 |
+
logits, _ = self.optimized_sampling(base_logits, hidden_states, attention_mask, guide_steps)
|
| 110 |
+
return logits
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def sample_guidance(self, tokens, guide_steps, diffusion_steps,
|
| 114 |
+
kappa_fn=lambda t: t, eta=1.0, alpha=1.0):
|
| 115 |
+
"""
|
| 116 |
+
Confidence-based progressive-unmasking denoising (as in the unconditional sampler),
|
| 117 |
+
but each step uses classifier-guided logits instead of the raw model logits.
|
| 118 |
+
|
| 119 |
+
Args:
|
| 120 |
+
kappa_fn: unmasking schedule, kappa(t) in [0,1]; fraction of positions committed by step t.
|
| 121 |
+
eta: re-masking scale applied to already-unmasked candidate positions.
|
| 122 |
+
alpha: blends token log-prob (alpha=1) and negative entropy (alpha=0) in the score.
|
| 123 |
+
"""
|
| 124 |
+
tau = self.config.guidance.sampling_temperature
|
| 125 |
+
|
| 126 |
+
xt = tokens['input_ids'].to(self.device)
|
| 127 |
+
attention_mask = torch.ones_like(xt).to(self.device)
|
| 128 |
+
|
| 129 |
+
dt = 1 / diffusion_steps
|
| 130 |
+
fix_mask = (xt != self.tokenizer.mask_token_id) # would be none for de novo setting
|
| 131 |
+
|
| 132 |
+
x0 = xt
|
| 133 |
+
for i in range(1, diffusion_steps + 1):
|
| 134 |
+
kappa_t = kappa_fn(i * dt)
|
| 135 |
+
|
| 136 |
+
logits = self.guided_logits(xt, attention_mask, guide_steps)
|
| 137 |
+
|
| 138 |
+
with torch.no_grad():
|
| 139 |
+
last_mask = xt == self.tokenizer.mask_token_id
|
| 140 |
+
unmask_t = ~last_mask & ~fix_mask
|
| 141 |
+
|
| 142 |
+
x0, logp = self.stochastic_sample_from_categorical(logits, tau)
|
| 143 |
+
|
| 144 |
+
entropy = torch.distributions.Categorical(logits=logits).entropy()
|
| 145 |
+
score = alpha * logp + (1 - alpha) * -entropy
|
| 146 |
+
score = score.masked_fill(fix_mask, float('inf')) # never remask fixed tokens
|
| 147 |
+
score[unmask_t] = score[unmask_t] * eta
|
| 148 |
+
|
| 149 |
+
num_to_mask = ((~fix_mask).sum(1, keepdim=True).float() * (1 - kappa_t)).long()
|
| 150 |
+
lowest_k_mask = self.topk_lowest_masking(score, num_to_mask)
|
| 151 |
+
|
| 152 |
+
xt[lowest_k_mask] = self.tokenizer.mask_token_id # remask lowest-confidence
|
| 153 |
+
mask_2_x0 = last_mask & ~lowest_k_mask # commit newly-confident positions
|
| 154 |
+
xt[mask_2_x0] = x0[mask_2_x0]
|
| 155 |
+
|
| 156 |
+
xt[xt == self.tokenizer.mask_token_id] = x0[xt == self.tokenizer.mask_token_id] # commit remaining
|
| 157 |
+
|
| 158 |
+
seq = xt.squeeze()[1:-1]
|
| 159 |
+
optim_tokens = self.tokenizer.decode(seq, skip_special_tokens=True).replace(" ", "")
|
| 160 |
+
|
| 161 |
+
with torch.no_grad():
|
| 162 |
+
final_hidden = self.diffusion(xt, attention_mask, return_hidden=True)
|
| 163 |
+
final_hidden = final_hidden.unsqueeze(0) if final_hidden.ndim != 3 else final_hidden
|
| 164 |
+
final_preds = self.classifier_score(final_hidden, attention_mask)
|
| 165 |
+
|
| 166 |
+
return optim_tokens, torch.sigmoid(final_preds).item()
|
src/sampling/olig_generator.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
import random
|
| 7 |
+
import torch
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from transformers import AutoTokenizer, AutoModelForMaskedLM
|
| 14 |
+
|
| 15 |
+
from src.lm.memdlm.diffusion_module import MembraneDiffusion
|
| 16 |
+
from src.sampling.olig_sampler import NOSSampler
|
| 17 |
+
|
| 18 |
+
from src.utils.generate_utils import calc_blosum_score, calc_ppl
|
| 19 |
+
from src.utils.model_utils import _print
|
| 20 |
+
from src.utils.config_utils import load_config, repo_path
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 24 |
+
config = load_config("oligo.yaml")
|
| 25 |
+
|
| 26 |
+
date = datetime.now().strftime("%Y-%m-%d")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def main():
|
| 32 |
+
csv_save_path = repo_path('results', 'oligo', config.wandb.name, date)
|
| 33 |
+
|
| 34 |
+
try: os.makedirs(csv_save_path, exist_ok=False)
|
| 35 |
+
except FileExistsError: pass
|
| 36 |
+
|
| 37 |
+
tokenizer = AutoTokenizer.from_pretrained(config.lm.pretrained_evoflow)
|
| 38 |
+
|
| 39 |
+
memdlm = MembraneDiffusion(config).to(device)
|
| 40 |
+
state_dict = memdlm.get_state_dict(str(repo_path("checkpoints", config.lm.ft_evoflow, "best_model.ckpt")))
|
| 41 |
+
memdlm.load_state_dict(state_dict)
|
| 42 |
+
memdlm.eval()
|
| 43 |
+
|
| 44 |
+
esm_pth = config.lm.pretrained_esm
|
| 45 |
+
esm_model = AutoModelForMaskedLM.from_pretrained(esm_pth).to(device)
|
| 46 |
+
esm_model.eval()
|
| 47 |
+
|
| 48 |
+
generator = NOSSampler(config, device, memdlm, esm_model, tokenizer)
|
| 49 |
+
|
| 50 |
+
# Determine length from positive controls
|
| 51 |
+
df = pd.read_csv(str(repo_path('data', 'olig_clf', 'test.csv')))
|
| 52 |
+
seqs = df['Sequence'].tolist()
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
generation_results = []
|
| 56 |
+
for seq in tqdm(seqs, desc=f"Generating sequences: "):
|
| 57 |
+
seq_res = []
|
| 58 |
+
|
| 59 |
+
seq_len = len(seq)
|
| 60 |
+
tokens = tokenizer(seq, return_tensors='pt')
|
| 61 |
+
|
| 62 |
+
gen_seq = ""
|
| 63 |
+
attempts = 0
|
| 64 |
+
|
| 65 |
+
while len(gen_seq) != seq_len and attempts < 3:
|
| 66 |
+
gen_seq, og_pred, final_pred = generator.sample_guidance(
|
| 67 |
+
tokens,
|
| 68 |
+
config.olig_guidance.guide_steps,
|
| 69 |
+
config.olig_guidance.diffusion_steps
|
| 70 |
+
)
|
| 71 |
+
attempts += 1
|
| 72 |
+
|
| 73 |
+
if len(gen_seq) != seq_len:
|
| 74 |
+
esm_ppl, memdlm_ppl = None, None
|
| 75 |
+
else:
|
| 76 |
+
esm_ppl = calc_ppl(esm_model, tokenizer, gen_seq, [i for i in range(len(gen_seq))], model_type='esm')
|
| 77 |
+
memdlm_ppl = calc_ppl(memdlm, tokenizer, gen_seq, [i for i in range(len(gen_seq))], model_type='diffusion')
|
| 78 |
+
blosum = calc_blosum_score(seq, gen_seq, indices=[i for i in range(len(gen_seq))])
|
| 79 |
+
|
| 80 |
+
seq_res.append(seq)
|
| 81 |
+
seq_res.append(gen_seq)
|
| 82 |
+
seq_res.append(og_pred)
|
| 83 |
+
seq_res.append(final_pred)
|
| 84 |
+
seq_res.append(final_pred - og_pred)
|
| 85 |
+
seq_res.append(esm_ppl)
|
| 86 |
+
seq_res.append(memdlm_ppl)
|
| 87 |
+
seq_res.append(blosum)
|
| 88 |
+
generation_results.append(seq_res)
|
| 89 |
+
|
| 90 |
+
df = pd.DataFrame(generation_results, columns=['Original Sequence', 'Generated Sequence', 'OG Olig Value', 'New Olig Value', 'Olig Increase', 'ESM PPL', 'MeMDLM PPL', 'MemDLM Blosum'])
|
| 91 |
+
df.to_csv(str(csv_save_path / "seqs_with_ppl.csv"), index=False)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
if __name__ == "__main__":
|
| 95 |
+
main()
|
src/sampling/olig_sampler.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
from torch import nn
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
from transformers import AutoModelForMaskedLM, AutoTokenizer
|
| 6 |
+
|
| 7 |
+
from src.guidance.oligo.oligo_module import OligomerClassifier
|
| 8 |
+
from src.sampling.unconditional_sampler import UnconditionalSampler
|
| 9 |
+
from src.lm.memdlm.diffusion_module import MembraneDiffusion
|
| 10 |
+
from src.utils.model_utils import _print
|
| 11 |
+
from src.utils.config_utils import repo_path
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class NOSSampler:
|
| 16 |
+
def __init__(self, config, device, mdlm, esm, tokenizer):
|
| 17 |
+
self.config = config
|
| 18 |
+
self.device = device
|
| 19 |
+
|
| 20 |
+
self.diffusion = mdlm
|
| 21 |
+
self.memdlm_lm = self.diffusion.model.lm_head
|
| 22 |
+
self.tokenizer = self.diffusion.tokenizer
|
| 23 |
+
|
| 24 |
+
ckpt_path = str(repo_path("checkpoints", config.wandb.name, "best_model.ckpt"))
|
| 25 |
+
self.classifier_model = OligomerClassifier(config).eval().to(self.device)
|
| 26 |
+
state_dict = self.classifier_model.get_state_dict(ckpt_path)
|
| 27 |
+
self.classifier_model.load_state_dict(state_dict)
|
| 28 |
+
|
| 29 |
+
self.SPECIAL_TOKEN_IDS = {0, 1, 2, 3, 29}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def sample_from_categorical(self, logits):
|
| 33 |
+
gumbel_noise = -torch.log(-torch.log(torch.rand_like(logits) + 1e-8) + 1e-8)
|
| 34 |
+
logits += gumbel_noise
|
| 35 |
+
log_probs = F.log_softmax(logits, dim=-1)
|
| 36 |
+
_, tokens = log_probs.max(dim=-1)
|
| 37 |
+
return tokens, log_probs
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def get_clf_preds(self, hidden_states, attention_mask):
|
| 41 |
+
"""Obtain diffusion model logits and classifier predictions from hidden states"""
|
| 42 |
+
batch = {"embeds": hidden_states.squeeze(), "attention_mask": attention_mask.squeeze()}
|
| 43 |
+
preds = self.classifier_model(batch)
|
| 44 |
+
return self.memdlm_lm(hidden_states), preds
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def embed_and_run_clf(self, input_ids, attention_masks):
|
| 48 |
+
"""Get sequence embeddings and classifier model predictions"""
|
| 49 |
+
outputs = self.esm(input_ids=input_ids, attention_mask=attention_masks)
|
| 50 |
+
sequence_embeddings = outputs.last_hidden_state.squeeze(0)
|
| 51 |
+
|
| 52 |
+
batch = {"embeds": sequence_embeddings, "attention_mask": attention_masks}
|
| 53 |
+
preds = self.classifier_model(batch)
|
| 54 |
+
|
| 55 |
+
return {
|
| 56 |
+
"clf_preds": preds.requires_grad_(True), # Enable gradients for backprop
|
| 57 |
+
"embeds": sequence_embeddings
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def compute_saliency(self, embeddings, attention_masks):
|
| 62 |
+
"""
|
| 63 |
+
Compute a saliency map using gradients as defined in LaMBO-2 (https://arxiv.org/pdf/2305.20009)
|
| 64 |
+
"""
|
| 65 |
+
embeddings = embeddings.detach().requires_grad_(True)
|
| 66 |
+
batch = {
|
| 67 |
+
"embeds": embeddings.squeeze(),
|
| 68 |
+
"attention_mask": attention_masks.squeeze()
|
| 69 |
+
}
|
| 70 |
+
out = self.classifier_model(batch).sum()
|
| 71 |
+
out.backward(retain_graph=True)
|
| 72 |
+
|
| 73 |
+
# Creating the saliency map (Eq.5 in LaMBO-2 paper)
|
| 74 |
+
grads = embeddings.grad.abs().sum(dim=-1) # Summation across hidden dim. Abs value for mangitude only
|
| 75 |
+
saliency = grads.pow(1.0 / self.config.olig_guidance.temperature).clamp(min=self.config.olig_guidance.eps)
|
| 76 |
+
return saliency.squeeze()
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def determine_edit_positions(self, saliency_map, preds):
|
| 80 |
+
"""
|
| 81 |
+
Create a one-hot mask that indicates the top-k low-value residue positions.
|
| 82 |
+
We defind low-value positions as those with high saliency scores and
|
| 83 |
+
thus a high edit probability.
|
| 84 |
+
"""
|
| 85 |
+
probabilities = saliency_map.masked_fill(preds >= self.config.olig_guidance.residue_thresh, 0.0) # exclude high-value tokens
|
| 86 |
+
probabilities = probabilities / probabilities.sum()
|
| 87 |
+
|
| 88 |
+
nonzero = probabilities.count_nonzero().item()
|
| 89 |
+
seq_len = probabilities.shape[0]
|
| 90 |
+
topk = max(1, int(seq_len * self.config.olig_guidance.topk_frac))
|
| 91 |
+
_, topk_edit_pos = torch.topk(probabilities, min(topk, nonzero))
|
| 92 |
+
|
| 93 |
+
mask = torch.zeros_like(probabilities).scatter(0, topk_edit_pos, torch.ones_like(probabilities))
|
| 94 |
+
return mask.unsqueeze(-1)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def update_logits(self, og_hidden, og_logits, hidden_state_mask, attention_mask, optimizer, delta):
|
| 98 |
+
"""
|
| 99 |
+
Shift logits distribution towards only high-quality residues by applying the explore-exploit loss.
|
| 100 |
+
"""
|
| 101 |
+
lamb = self.config.olig_guidance.reg_strength
|
| 102 |
+
|
| 103 |
+
h_current = og_hidden + hidden_state_mask * delta
|
| 104 |
+
new_logits, v_ht_prime = self.get_clf_preds(h_current, attention_mask)
|
| 105 |
+
|
| 106 |
+
kl = F.kl_div(
|
| 107 |
+
F.log_softmax(new_logits, dim=-1),
|
| 108 |
+
F.softmax(og_logits, dim=-1),
|
| 109 |
+
reduction='sum'
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
loss = lamb * kl - v_ht_prime.sum()
|
| 113 |
+
loss.backward(retain_graph=True)
|
| 114 |
+
optimizer.step()
|
| 115 |
+
optimizer.zero_grad()
|
| 116 |
+
|
| 117 |
+
return delta
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def optimized_sampling(self, og_logits, og_hidden, attention_mask, n_steps):
|
| 121 |
+
"""Main entry point to optimize a generated sequence."""
|
| 122 |
+
eta = self.config.olig_guidance.step_size
|
| 123 |
+
|
| 124 |
+
# Calculate initial clf predictions
|
| 125 |
+
batch = {"embeds": og_hidden, "attention_mask": attention_mask}
|
| 126 |
+
preds = self.classifier_model(batch)
|
| 127 |
+
|
| 128 |
+
delta = nn.Parameter(torch.zeros_like(og_hidden), requires_grad=True)
|
| 129 |
+
optimizer = torch.optim.Adagrad([delta], lr=eta)
|
| 130 |
+
optimizer.zero_grad()
|
| 131 |
+
|
| 132 |
+
with torch.enable_grad():
|
| 133 |
+
for n in range(n_steps):
|
| 134 |
+
|
| 135 |
+
# Compute saliency map and edit positions using updated hidden states
|
| 136 |
+
saliency_map = self.compute_saliency(og_hidden + delta.data, attention_mask)
|
| 137 |
+
|
| 138 |
+
# One-hot mask that encodes the saliency-selected edit positions
|
| 139 |
+
mask = self.determine_edit_positions(saliency_map, preds)
|
| 140 |
+
|
| 141 |
+
# Optimize and generate the new sequence
|
| 142 |
+
delta = self.update_logits(
|
| 143 |
+
og_hidden=og_hidden,
|
| 144 |
+
og_logits=og_logits,
|
| 145 |
+
hidden_state_mask=mask,
|
| 146 |
+
attention_mask=attention_mask,
|
| 147 |
+
optimizer=optimizer,
|
| 148 |
+
delta=delta
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
h_new = og_hidden + delta.data
|
| 152 |
+
new_logits, _ = self.get_clf_preds(h_new, attention_mask)
|
| 153 |
+
|
| 154 |
+
return new_logits, h_new
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def sample_guidance(self, tokens, guide_steps, diffusion_steps):
|
| 158 |
+
x = tokens['input_ids'].to(self.device)
|
| 159 |
+
attention_mask = tokens['attention_mask'].to(self.device)
|
| 160 |
+
|
| 161 |
+
og_pred = self.classifier_model({"input_ids": x, "attention_mask": attention_mask,})
|
| 162 |
+
|
| 163 |
+
for _ in range(diffusion_steps):
|
| 164 |
+
hidden_states = self.diffusion(x, attention_mask, return_hidden=True)
|
| 165 |
+
logits = self.memdlm_lm(hidden_states)
|
| 166 |
+
hidden_states = hidden_states.unsqueeze(0) if hidden_states.ndim != 3 else hidden_states
|
| 167 |
+
|
| 168 |
+
logits, hidden_states = self.optimized_sampling(logits, hidden_states, attention_mask, guide_steps)
|
| 169 |
+
logits = self.memdlm_lm(hidden_states)
|
| 170 |
+
x, _ = self.sample_from_categorical(logits)
|
| 171 |
+
|
| 172 |
+
seq = x.squeeze()
|
| 173 |
+
# _print(seq)
|
| 174 |
+
# start = 1 if seq[0].item() in self.SPECIAL_TOKEN_IDS else 0
|
| 175 |
+
# end = -1 if seq[-1].item() in self.SPECIAL_TOKEN_IDS else len(seq)
|
| 176 |
+
# seq = seq[start:end]
|
| 177 |
+
# _print(seq)
|
| 178 |
+
|
| 179 |
+
_print(seq)
|
| 180 |
+
seq = seq[1:-1]
|
| 181 |
+
_print(seq)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
optim_tokens = self.tokenizer.decode(seq, skip_special_tokens=True).replace(" ", "")
|
| 185 |
+
final_pred = self.classifier_model({"embeds": hidden_states, "attention_mask": attention_mask,})
|
| 186 |
+
|
| 187 |
+
return optim_tokens, F.sigmoid(og_pred).item(), F.sigmoid(final_pred).item()
|
src/sampling/pet_generator.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import torch
|
| 6 |
+
import pandas as pd
|
| 7 |
+
from tqdm import tqdm
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from omegaconf import OmegaConf
|
| 10 |
+
from transformers import AutoTokenizer, AutoModelForMaskedLM
|
| 11 |
+
|
| 12 |
+
from src.lm.memdlm.diffusion_module import MembraneDiffusion
|
| 13 |
+
from src.sampling.pet_sampler import PETSampler
|
| 14 |
+
from src.utils.generate_utils import (
|
| 15 |
+
mask_for_scaffold,
|
| 16 |
+
calc_blosum_score,
|
| 17 |
+
calc_ppl,
|
| 18 |
+
calc_tm_enrich
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
from src.utils.model_utils import _print
|
| 22 |
+
from src.utils.config_utils import load_config, repo_path
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
config = load_config("solubility.yaml")
|
| 26 |
+
task = config.guidance.get("task", "solubilize")
|
| 27 |
+
|
| 28 |
+
results_root = repo_path("results", "heme", config.lm.ft_evoflow)
|
| 29 |
+
todays_date = datetime.today().strftime('%Y-%m-%d')
|
| 30 |
+
|
| 31 |
+
if config.guidance.prior == 'boltzmann':
|
| 32 |
+
csv_save_path = results_root / task / todays_date / f"{config.guidance.prior}-t={config.guidance.boltzmann_t}_p={config.guidance.top_p}_tau={config.guidance.sampling_t}"
|
| 33 |
+
elif config.guidance.prior == 'lm_probs':
|
| 34 |
+
csv_save_path = results_root / task / todays_date / f"{config.guidance.prior}_p={config.guidance.top_p}_tau={config.guidance.sampling_t}"
|
| 35 |
+
|
| 36 |
+
try: os.makedirs(csv_save_path, exist_ok=False)
|
| 37 |
+
except FileExistsError: pass
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def main():
|
| 41 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 42 |
+
|
| 43 |
+
tokenizer = AutoTokenizer.from_pretrained(config.lm.pretrained_esm)
|
| 44 |
+
esm_model = AutoModelForMaskedLM.from_pretrained(config.lm.pretrained_esm).eval().to(device)
|
| 45 |
+
|
| 46 |
+
diffusion = MembraneDiffusion(config).to(device)
|
| 47 |
+
state_dict = diffusion.get_state_dict(str(repo_path("checkpoints", config.lm.ft_evoflow, "best_model.ckpt")))
|
| 48 |
+
diffusion.load_state_dict(state_dict)
|
| 49 |
+
diffusion.eval().to(device)
|
| 50 |
+
|
| 51 |
+
sampler = PETSampler(config, esm_model, tokenizer, diffusion, device)
|
| 52 |
+
|
| 53 |
+
# Update this path to your input CSV of scaffold sequences (uppercase = soluble, lowercase = TM).
|
| 54 |
+
df = pd.read_csv(str(repo_path("results", "heme", "cybtx.csv")))
|
| 55 |
+
sequences = df['Sequence'].tolist()
|
| 56 |
+
|
| 57 |
+
gen_seqs, ppls, blosums, og_tms, gen_tms, delta_tms = [], [], [], [], [], []
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
for seq in tqdm(sequences, desc='Infilling Sequences'):
|
| 61 |
+
masked_seq = mask_for_scaffold(seq, generate_type='lowercase', mask_token='<mask>')
|
| 62 |
+
tokens = tokenizer(masked_seq, return_tensors='pt')
|
| 63 |
+
input_ids, attn_masks = tokens['input_ids'].to(device), tokens['attention_mask'].to(device)
|
| 64 |
+
|
| 65 |
+
tm_idxs = [i for i in range(len(seq)) if seq[i].islower()] # keep same for blosum calcs, etc
|
| 66 |
+
soluble_idxs = [i + 1 for i in range(len(seq)) if seq[i].isupper()] # 1 offset for cls token
|
| 67 |
+
|
| 68 |
+
infilled_tokens = sampler.optimize_sequence(
|
| 69 |
+
input_ids=input_ids,
|
| 70 |
+
attn_masks=attn_masks,
|
| 71 |
+
soluble_indices=soluble_idxs,
|
| 72 |
+
)
|
| 73 |
+
infilled_seq = tokenizer.decode(infilled_tokens).replace(" ", "")[5:-5]
|
| 74 |
+
|
| 75 |
+
try:
|
| 76 |
+
bl = calc_blosum_score(seq.upper(), infilled_seq, tm_idxs)
|
| 77 |
+
except:
|
| 78 |
+
bl = float('inf')
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
ppl = calc_ppl(esm_model, tokenizer, infilled_seq, [i for i in range(len(seq))], model_type='esm')
|
| 82 |
+
except:
|
| 83 |
+
ppl = float('inf')
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
og_tm, gen_tm, delta_tm = calc_tm_enrich(seq.upper(), infilled_seq, tm_idxs)
|
| 87 |
+
except:
|
| 88 |
+
og_tm, gen_tm, delta_tm = float('inf'), float('inf'), float('inf')
|
| 89 |
+
|
| 90 |
+
gen_seqs.append(infilled_seq)
|
| 91 |
+
ppls.append(ppl)
|
| 92 |
+
blosums.append(bl)
|
| 93 |
+
og_tms.append(og_tm)
|
| 94 |
+
gen_tms.append(gen_tm)
|
| 95 |
+
delta_tms.append(delta_tm)
|
| 96 |
+
|
| 97 |
+
_print(seq)
|
| 98 |
+
_print(infilled_seq)
|
| 99 |
+
_print(ppl)
|
| 100 |
+
_print(bl)
|
| 101 |
+
_print(og_tm)
|
| 102 |
+
_print(gen_tm)
|
| 103 |
+
_print(delta_tm)
|
| 104 |
+
_print('\n')
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
df['MeMDLM Sequence'] = gen_seqs
|
| 108 |
+
df['MeMDLM PPL'] = ppls
|
| 109 |
+
df['MeMDLM BLOSUM'] = blosums
|
| 110 |
+
df['OG TM Enrichment'] = og_tms
|
| 111 |
+
df['MeMDLM TM Enrichment'] = gen_tms
|
| 112 |
+
df['Delta TM Enrichment'] = delta_tms
|
| 113 |
+
|
| 114 |
+
_print(df)
|
| 115 |
+
df.to_csv(csv_save_path / "infilled_seqs.csv", index=False)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
if __name__ == "__main__":
|
| 120 |
+
main()
|
| 121 |
+
|
src/sampling/{guided_sampler.py → pet_sampler.py}
RENAMED
|
@@ -4,11 +4,12 @@ import torch
|
|
| 4 |
import torch.nn.functional as F
|
| 5 |
|
| 6 |
from src.utils.model_utils import _print
|
| 7 |
-
from src.
|
|
|
|
| 8 |
from src.sampling.unconditional_sampler import UnconditionalSampler
|
| 9 |
|
| 10 |
|
| 11 |
-
class
|
| 12 |
def __init__(self, config, esm_model, tokenizer, diffusion, device):
|
| 13 |
self.config = config
|
| 14 |
self.device = device
|
|
@@ -18,7 +19,7 @@ class GuidedSampler:
|
|
| 18 |
self.tokenizer = tokenizer
|
| 19 |
self.uncond_generator = UnconditionalSampler(self.tokenizer, self.memdlm)
|
| 20 |
|
| 21 |
-
ckpt_path =
|
| 22 |
self.classifier_model = SolubilityClassifier(config)
|
| 23 |
state_dict = self.classifier_model.get_state_dict(ckpt_path)
|
| 24 |
self.classifier_model.load_state_dict(state_dict)
|
|
@@ -31,7 +32,7 @@ class GuidedSampler:
|
|
| 31 |
self.saliency_t = self.config.guidance.saliency_t
|
| 32 |
self.sampling_t = self.config.guidance.sampling_t
|
| 33 |
self.boltzmann_t = self.config.guidance.boltzmann_t
|
| 34 |
-
|
| 35 |
|
| 36 |
def embed_sequence(self, input_ids, attention_masks):
|
| 37 |
with torch.no_grad():
|
|
@@ -107,7 +108,13 @@ class GuidedSampler:
|
|
| 107 |
bias[other_idxs] = 0.0
|
| 108 |
|
| 109 |
sol_scores = torch.sigmoid(solubility_logits)
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
lm_probs = F.softmax(logits_prior / self.sampling_t, dim=-1)
|
| 113 |
boltz_weight = torch.exp(token_bias / self.boltzmann_t)
|
|
@@ -143,6 +150,10 @@ class GuidedSampler:
|
|
| 143 |
|
| 144 |
# Initialize a mask to store the editable token positions
|
| 145 |
edit_mask = torch.ones(seq_len, dtype=torch.bool, device=self.device)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
# Check for any provided soluble residues, otherwise use classifier preds
|
| 148 |
if len(soluble_indices) > 0:
|
|
@@ -151,10 +162,12 @@ class GuidedSampler:
|
|
| 151 |
solubility_preds = F.sigmoid(solubility_logits)
|
| 152 |
edit_mask[solubility_preds > 0.5] = False
|
| 153 |
|
| 154 |
-
#
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
| 158 |
|
| 159 |
edit_idxs = edit_mask.nonzero(as_tuple=True)[0]
|
| 160 |
return edit_idxs
|
|
@@ -207,6 +220,7 @@ class GuidedSampler:
|
|
| 207 |
Compute the log probs of the "new" (optimized) token.
|
| 208 |
"""
|
| 209 |
w = torch.sigmoid(saliency_weight * self.alpha) # Between [0, 1] to ensure valid probs
|
|
|
|
| 210 |
p_lm = torch.exp(logp_lm)
|
| 211 |
p_prior = torch.exp(logp_prior)
|
| 212 |
mixed_probs = (1 - w) * p_lm + w * p_prior
|
|
@@ -225,8 +239,12 @@ class GuidedSampler:
|
|
| 225 |
def optimize_sequence(self, input_ids, attn_masks, soluble_indices):
|
| 226 |
_print(f'soluble idx: {soluble_indices}')
|
| 227 |
|
|
|
|
|
|
|
| 228 |
# Initialize token ids, logits, and log probs of sequence
|
| 229 |
x0, logp_lm, logits_prior = self.denoise_sequence(input_ids, attn_masks)
|
|
|
|
|
|
|
| 230 |
_print(f'og tokens: {x0}')
|
| 231 |
_print(f'og tokens: {x0.shape}')
|
| 232 |
_print(f'og log probs: {logp_lm.shape}')
|
|
@@ -281,11 +299,12 @@ class GuidedSampler:
|
|
| 281 |
# Sample new tokens
|
| 282 |
x0_prime = torch.distributions.Categorical(logits=logp_lm).sample()
|
| 283 |
|
| 284 |
-
# Check if any
|
| 285 |
-
self.check_scaffold(
|
| 286 |
|
| 287 |
-
# Preserve
|
| 288 |
-
|
| 289 |
-
|
|
|
|
| 290 |
|
| 291 |
return x0_prime
|
|
|
|
| 4 |
import torch.nn.functional as F
|
| 5 |
|
| 6 |
from src.utils.model_utils import _print
|
| 7 |
+
from src.utils.config_utils import repo_path
|
| 8 |
+
from src.guidance.solubility.solubility_module import SolubilityClassifier
|
| 9 |
from src.sampling.unconditional_sampler import UnconditionalSampler
|
| 10 |
|
| 11 |
|
| 12 |
+
class PETSampler:
|
| 13 |
def __init__(self, config, esm_model, tokenizer, diffusion, device):
|
| 14 |
self.config = config
|
| 15 |
self.device = device
|
|
|
|
| 19 |
self.tokenizer = tokenizer
|
| 20 |
self.uncond_generator = UnconditionalSampler(self.tokenizer, self.memdlm)
|
| 21 |
|
| 22 |
+
ckpt_path = str(repo_path("checkpoints", config.wandb.name, "best_model.ckpt"))
|
| 23 |
self.classifier_model = SolubilityClassifier(config)
|
| 24 |
state_dict = self.classifier_model.get_state_dict(ckpt_path)
|
| 25 |
self.classifier_model.load_state_dict(state_dict)
|
|
|
|
| 32 |
self.saliency_t = self.config.guidance.saliency_t
|
| 33 |
self.sampling_t = self.config.guidance.sampling_t
|
| 34 |
self.boltzmann_t = self.config.guidance.boltzmann_t
|
| 35 |
+
self.task = self.config.guidance.get("task", "solubilize")
|
| 36 |
|
| 37 |
def embed_sequence(self, input_ids, attention_masks):
|
| 38 |
with torch.no_grad():
|
|
|
|
| 108 |
bias[other_idxs] = 0.0
|
| 109 |
|
| 110 |
sol_scores = torch.sigmoid(solubility_logits)
|
| 111 |
+
task = self.task
|
| 112 |
+
if task == "desolubilize":
|
| 113 |
+
# At soluble positions, bias toward hydrophobic / TM-like residues.
|
| 114 |
+
token_bias = sol_scores.unsqueeze(-1) * (-bias)
|
| 115 |
+
else:
|
| 116 |
+
# At insoluble positions, bias toward hydrophilic residues.
|
| 117 |
+
token_bias = (1.0 - sol_scores).unsqueeze(-1) * bias
|
| 118 |
|
| 119 |
lm_probs = F.softmax(logits_prior / self.sampling_t, dim=-1)
|
| 120 |
boltz_weight = torch.exp(token_bias / self.boltzmann_t)
|
|
|
|
| 150 |
|
| 151 |
# Initialize a mask to store the editable token positions
|
| 152 |
edit_mask = torch.ones(seq_len, dtype=torch.bool, device=self.device)
|
| 153 |
+
|
| 154 |
+
# ignore <cls> and <eos> tokens
|
| 155 |
+
edit_mask[0] = False
|
| 156 |
+
edit_mask[-1] = False
|
| 157 |
|
| 158 |
# Check for any provided soluble residues, otherwise use classifier preds
|
| 159 |
if len(soluble_indices) > 0:
|
|
|
|
| 162 |
solubility_preds = F.sigmoid(solubility_logits)
|
| 163 |
edit_mask[solubility_preds > 0.5] = False
|
| 164 |
|
| 165 |
+
# during solubilization, we also fix the high-saliency TM residues
|
| 166 |
+
# but for desolubilization, skip this as high-saliency positions are the soluble sites we want to edit
|
| 167 |
+
if self.task != "desolubilize":
|
| 168 |
+
num_conserved = max(1, int(0.1 * edit_mask.sum()))
|
| 169 |
+
_, topk_idxs = torch.topk(saliency_map, num_conserved)
|
| 170 |
+
edit_mask[topk_idxs] = False
|
| 171 |
|
| 172 |
edit_idxs = edit_mask.nonzero(as_tuple=True)[0]
|
| 173 |
return edit_idxs
|
|
|
|
| 220 |
Compute the log probs of the "new" (optimized) token.
|
| 221 |
"""
|
| 222 |
w = torch.sigmoid(saliency_weight * self.alpha) # Between [0, 1] to ensure valid probs
|
| 223 |
+
_print(f'w: {w}')
|
| 224 |
p_lm = torch.exp(logp_lm)
|
| 225 |
p_prior = torch.exp(logp_prior)
|
| 226 |
mixed_probs = (1 - w) * p_lm + w * p_prior
|
|
|
|
| 239 |
def optimize_sequence(self, input_ids, attn_masks, soluble_indices):
|
| 240 |
_print(f'soluble idx: {soluble_indices}')
|
| 241 |
|
| 242 |
+
scaffold_ids = input_ids.squeeze()
|
| 243 |
+
|
| 244 |
# Initialize token ids, logits, and log probs of sequence
|
| 245 |
x0, logp_lm, logits_prior = self.denoise_sequence(input_ids, attn_masks)
|
| 246 |
+
# x0 is resampled from LM logits at all positions, so restore scaffold from input
|
| 247 |
+
x0[soluble_indices] = scaffold_ids[soluble_indices]
|
| 248 |
_print(f'og tokens: {x0}')
|
| 249 |
_print(f'og tokens: {x0.shape}')
|
| 250 |
_print(f'og log probs: {logp_lm.shape}')
|
|
|
|
| 299 |
# Sample new tokens
|
| 300 |
x0_prime = torch.distributions.Categorical(logits=logp_lm).sample()
|
| 301 |
|
| 302 |
+
# Check if any scaffold residues have been changed
|
| 303 |
+
self.check_scaffold(scaffold_ids, x0_prime, soluble_indices)
|
| 304 |
|
| 305 |
+
# Preserve scaffold from the _original_ tokenized input, not resampled x0
|
| 306 |
+
# since the categorical sampling might not sample the original scaffold token
|
| 307 |
+
x0_prime[soluble_indices] = scaffold_ids[soluble_indices]
|
| 308 |
+
self.check_scaffold(scaffold_ids, x0_prime, soluble_indices)
|
| 309 |
|
| 310 |
return x0_prime
|
src/sampling/unconditional_generator.py
CHANGED
|
@@ -14,15 +14,15 @@ from omegaconf import OmegaConf
|
|
| 14 |
from datetime import datetime
|
| 15 |
from transformers import AutoTokenizer, AutoModelForMaskedLM
|
| 16 |
|
| 17 |
-
from
|
| 18 |
from src.sampling.unconditional_sampler import UnconditionalSampler
|
| 19 |
from src.utils.generate_utils import mask_for_de_novo, calc_ppl
|
| 20 |
from src.utils.model_utils import _print
|
|
|
|
| 21 |
|
| 22 |
|
| 23 |
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 24 |
-
|
| 25 |
-
config = OmegaConf.load("./src/configs/lm.yaml")
|
| 26 |
|
| 27 |
date = datetime.now().strftime("%Y-%m-%d")
|
| 28 |
|
|
@@ -42,24 +42,23 @@ def generate_sequence(prior: str, tokenizer, generator, device):
|
|
| 42 |
|
| 43 |
|
| 44 |
def main():
|
| 45 |
-
csv_save_path =
|
| 46 |
|
| 47 |
try: os.makedirs(csv_save_path, exist_ok=False)
|
| 48 |
except FileExistsError: pass
|
| 49 |
|
| 50 |
-
|
| 51 |
tokenizer = AutoTokenizer.from_pretrained(config.lm.pretrained_evoflow)
|
| 52 |
|
| 53 |
-
|
| 54 |
-
state_dict =
|
| 55 |
-
|
| 56 |
-
|
| 57 |
|
| 58 |
esm_pth = config.lm.pretrained_esm
|
| 59 |
esm_model = AutoModelForMaskedLM.from_pretrained(esm_pth).to(device)
|
| 60 |
esm_model.eval()
|
| 61 |
|
| 62 |
-
generator = UnconditionalSampler(tokenizer,
|
| 63 |
|
| 64 |
# # Get 100 random sequence lengths to generate
|
| 65 |
# seq_lengths = [random.randint(50, 250) for _ in range(5000)]
|
|
@@ -69,14 +68,16 @@ def main():
|
|
| 69 |
# seq_lengths = [len(seq) for seq in df['Sequence'].tolist() for _ in range(500)] # generate each length 100 times
|
| 70 |
# _print(seq_lengths)
|
| 71 |
|
| 72 |
-
# Determine lengths from test set distribution
|
| 73 |
-
df = pd.read_csv("./data/test.csv")
|
| 74 |
-
seq_lengths = [len(seq) for seq in df['Sequence'].tolist()]
|
| 75 |
-
length_counts = Counter(seq_lengths) # {L1: freq, L2: freq, ...}
|
| 76 |
-
total = sum(length_counts.values()) # total number of tokens
|
| 77 |
-
lengths = np.array(list(length_counts.keys())) # Frequency of each length
|
| 78 |
-
probs = np.array([length_counts[l] / total for l in lengths])
|
| 79 |
-
seq_lengths = np.random.choice(lengths, size=len(seq_lengths), p=probs)
|
|
|
|
|
|
|
| 80 |
|
| 81 |
generation_results = []
|
| 82 |
for seq_len in tqdm(seq_lengths, desc=f"Generating sequences: "):
|
|
@@ -91,23 +92,23 @@ def main():
|
|
| 91 |
attempts += 1
|
| 92 |
|
| 93 |
if len(gen_seq) != seq_len:
|
| 94 |
-
esm_ppl,
|
| 95 |
else:
|
| 96 |
esm_ppl = calc_ppl(esm_model, tokenizer, gen_seq, [i for i in range(len(gen_seq))], model_type='esm')
|
| 97 |
-
|
| 98 |
|
| 99 |
_print(f'gen seq: {gen_seq}')
|
| 100 |
_print(f'esm ppl: {esm_ppl}')
|
| 101 |
-
_print(f'
|
| 102 |
|
| 103 |
seq_res.append(gen_seq)
|
| 104 |
seq_res.append(esm_ppl)
|
| 105 |
-
seq_res.append(
|
| 106 |
|
| 107 |
generation_results.append(seq_res)
|
| 108 |
|
| 109 |
-
df = pd.DataFrame(generation_results, columns=['Generated Sequence', 'ESM PPL', '
|
| 110 |
-
df.to_csv(csv_save_path
|
| 111 |
|
| 112 |
|
| 113 |
if __name__ == "__main__":
|
|
|
|
| 14 |
from datetime import datetime
|
| 15 |
from transformers import AutoTokenizer, AutoModelForMaskedLM
|
| 16 |
|
| 17 |
+
from src.lm.memdlm.diffusion_module import MembraneDiffusion
|
| 18 |
from src.sampling.unconditional_sampler import UnconditionalSampler
|
| 19 |
from src.utils.generate_utils import mask_for_de_novo, calc_ppl
|
| 20 |
from src.utils.model_utils import _print
|
| 21 |
+
from src.utils.config_utils import load_config, repo_path
|
| 22 |
|
| 23 |
|
| 24 |
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 25 |
+
config = load_config("lm.yaml")
|
|
|
|
| 26 |
|
| 27 |
date = datetime.now().strftime("%Y-%m-%d")
|
| 28 |
|
|
|
|
| 42 |
|
| 43 |
|
| 44 |
def main():
|
| 45 |
+
csv_save_path = repo_path('results', 'multipass', config.wandb.name, date)
|
| 46 |
|
| 47 |
try: os.makedirs(csv_save_path, exist_ok=False)
|
| 48 |
except FileExistsError: pass
|
| 49 |
|
|
|
|
| 50 |
tokenizer = AutoTokenizer.from_pretrained(config.lm.pretrained_evoflow)
|
| 51 |
|
| 52 |
+
memdlm = MembraneDiffusion(config).to(device)
|
| 53 |
+
state_dict = memdlm.get_state_dict(str(repo_path("checkpoints", config.wandb.name, "best_model.ckpt")))
|
| 54 |
+
memdlm.load_state_dict(state_dict)
|
| 55 |
+
memdlm.eval()
|
| 56 |
|
| 57 |
esm_pth = config.lm.pretrained_esm
|
| 58 |
esm_model = AutoModelForMaskedLM.from_pretrained(esm_pth).to(device)
|
| 59 |
esm_model.eval()
|
| 60 |
|
| 61 |
+
generator = UnconditionalSampler(tokenizer, memdlm)
|
| 62 |
|
| 63 |
# # Get 100 random sequence lengths to generate
|
| 64 |
# seq_lengths = [random.randint(50, 250) for _ in range(5000)]
|
|
|
|
| 68 |
# seq_lengths = [len(seq) for seq in df['Sequence'].tolist() for _ in range(500)] # generate each length 100 times
|
| 69 |
# _print(seq_lengths)
|
| 70 |
|
| 71 |
+
# # Determine lengths from test set distribution
|
| 72 |
+
# df = pd.read_csv("./data/test.csv")
|
| 73 |
+
# seq_lengths = [len(seq) for seq in df['Sequence'].tolist()]
|
| 74 |
+
# length_counts = Counter(seq_lengths) # {L1: freq, L2: freq, ...}
|
| 75 |
+
# total = sum(length_counts.values()) # total number of tokens
|
| 76 |
+
# lengths = np.array(list(length_counts.keys())) # Frequency of each length
|
| 77 |
+
# probs = np.array([length_counts[l] / total for l in lengths])
|
| 78 |
+
# seq_lengths = np.random.choice(lengths, size=len(seq_lengths), p=probs)
|
| 79 |
+
|
| 80 |
+
seq_lengths = [x for x in range(60, 161) for _ in range(3)]
|
| 81 |
|
| 82 |
generation_results = []
|
| 83 |
for seq_len in tqdm(seq_lengths, desc=f"Generating sequences: "):
|
|
|
|
| 92 |
attempts += 1
|
| 93 |
|
| 94 |
if len(gen_seq) != seq_len:
|
| 95 |
+
esm_ppl, memdlm_ppl = None, None
|
| 96 |
else:
|
| 97 |
esm_ppl = calc_ppl(esm_model, tokenizer, gen_seq, [i for i in range(len(gen_seq))], model_type='esm')
|
| 98 |
+
memdlm_ppl = calc_ppl(memdlm, tokenizer, gen_seq, [i for i in range(len(gen_seq))], model_type='diffusion')
|
| 99 |
|
| 100 |
_print(f'gen seq: {gen_seq}')
|
| 101 |
_print(f'esm ppl: {esm_ppl}')
|
| 102 |
+
_print(f'memdlm ppl: {memdlm_ppl}')
|
| 103 |
|
| 104 |
seq_res.append(gen_seq)
|
| 105 |
seq_res.append(esm_ppl)
|
| 106 |
+
seq_res.append(memdlm_ppl)
|
| 107 |
|
| 108 |
generation_results.append(seq_res)
|
| 109 |
|
| 110 |
+
df = pd.DataFrame(generation_results, columns=['Generated Sequence', 'ESM PPL', 'MeMDLM PPL'])
|
| 111 |
+
df.to_csv(str(csv_save_path / "seqs_with_ppl.csv"), index=False)
|
| 112 |
|
| 113 |
|
| 114 |
if __name__ == "__main__":
|
src/sampling/unconditional_sampler.py
CHANGED
|
@@ -15,7 +15,7 @@ class UnconditionalSampler:
|
|
| 15 |
self.seed_everything(seed=42)
|
| 16 |
|
| 17 |
@torch.inference_mode()
|
| 18 |
-
def sample_unconditional(self, xt, num_steps, tau=
|
| 19 |
"""
|
| 20 |
Stochastic remasking sampling method for iterative refinement of sequences.
|
| 21 |
|
|
|
|
| 15 |
self.seed_everything(seed=42)
|
| 16 |
|
| 17 |
@torch.inference_mode()
|
| 18 |
+
def sample_unconditional(self, xt, num_steps, tau=1.0, kappa_fn=lambda t: t, eta=1, alpha=1., banned_token_ids=None, return_logits=None):
|
| 19 |
"""
|
| 20 |
Stochastic remasking sampling method for iterative refinement of sequences.
|
| 21 |
|
src/utils/__init__.py
ADDED
|
File without changes
|
src/utils/config_utils.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
from omegaconf import OmegaConf
|
| 3 |
+
|
| 4 |
+
# Repository root (MeMDLM_v2/)
|
| 5 |
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def load_config(config_name: str):
|
| 9 |
+
"""Load a YAML config from src/configs/."""
|
| 10 |
+
return OmegaConf.load(REPO_ROOT / "src" / "configs" / config_name)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def repo_path(*parts: str) -> Path:
|
| 14 |
+
"""Build an absolute path relative to the repository root."""
|
| 15 |
+
return REPO_ROOT.joinpath(*parts)
|
src/utils/generate_utils.py
CHANGED
|
@@ -9,13 +9,13 @@ import numpy as np
|
|
| 9 |
from omegaconf import OmegaConf
|
| 10 |
from transformers import AutoModelForMaskedLM, AutoModel, AutoTokenizer
|
| 11 |
|
| 12 |
-
from src.lm.memdlm.diffusion_module import
|
| 13 |
-
from src.lm.dplm.diffusion_module import DPLM
|
| 14 |
from src.utils.model_utils import get_latents, _print
|
| 15 |
from src.sampling.unconditional_sampler import UnconditionalSampler
|
| 16 |
-
from src.lm.dplm.unconditional_sampler import UnconditionalSampler as DPLMUnconditionalSampler
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
| 19 |
|
| 20 |
# -------# Masking #-------- #
|
| 21 |
def mask_for_de_novo(sequence_length):
|
|
@@ -30,7 +30,7 @@ def mask_for_scaffold(sequence, generate_type, mask_token):
|
|
| 30 |
|
| 31 |
|
| 32 |
# -------# Generation #-------- #
|
| 33 |
-
def memflow_infill_uncond(masked_seq, tokenizer, model:
|
| 34 |
generator = UnconditionalSampler(tokenizer, model) # initialize the generator object
|
| 35 |
xt = tokenizer(masked_seq, return_tensors='pt')['input_ids'].to(model.device)
|
| 36 |
denoised_tokens = generator.sample_unconditional(xt, config.sampling.n_steps)[0].squeeze()
|
|
@@ -68,7 +68,10 @@ def evodiff_infill(motif_seq, tokenizer, model, device, batch_size=1):
|
|
| 68 |
return output[0] #if batch_size==1 else output, og_sample, loc
|
| 69 |
|
| 70 |
|
| 71 |
-
def dplm_infill(masked_seq, tokenizer, model
|
|
|
|
|
|
|
|
|
|
| 72 |
generator = DPLMUnconditionalSampler(tokenizer, model)
|
| 73 |
xt = tokenizer(masked_seq, return_tensors='pt')['input_ids'].to(model.device)
|
| 74 |
denoised_tokens = generator.sample_unconditional(xt, config.sampling.n_steps)[0].squeeze()
|
|
@@ -111,7 +114,7 @@ def calc_ppl(model, tokenizer, generated_sequence, mask_token_indices, model_typ
|
|
| 111 |
with torch.no_grad():
|
| 112 |
if model_type == 'esm':
|
| 113 |
loss = model(masked_input, labels=labels).loss.item()
|
| 114 |
-
elif model_type == '
|
| 115 |
logits = model.forward(masked_input, attention_mask=attn_mask)
|
| 116 |
loss = F.cross_entropy(
|
| 117 |
logits.view(-1, logits.size(-1)),
|
|
@@ -143,6 +146,28 @@ def calc_blosum_score(og_seq, gen_seq, indices):
|
|
| 143 |
return tot_score / len(indices) if indices else 0
|
| 144 |
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
def calc_cos_sim(original_sequence, generated_sequence, tokenizer, esm_model, device):
|
| 147 |
og_embeddings = get_latents(esm_model, tokenizer, original_sequence.upper(), device)
|
| 148 |
new_embeddings = get_latents(esm_model, tokenizer, generated_sequence, device)
|
|
|
|
| 9 |
from omegaconf import OmegaConf
|
| 10 |
from transformers import AutoModelForMaskedLM, AutoModel, AutoTokenizer
|
| 11 |
|
| 12 |
+
from src.lm.memdlm.diffusion_module import MembraneDiffusion
|
|
|
|
| 13 |
from src.utils.model_utils import get_latents, _print
|
| 14 |
from src.sampling.unconditional_sampler import UnconditionalSampler
|
|
|
|
| 15 |
|
| 16 |
+
|
| 17 |
+
config = OmegaConf.load("/scratch/pranamlab/sgoel/MeMDLM_v2/src/configs/lm.yaml")
|
| 18 |
+
|
| 19 |
|
| 20 |
# -------# Masking #-------- #
|
| 21 |
def mask_for_de_novo(sequence_length):
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
# -------# Generation #-------- #
|
| 33 |
+
def memflow_infill_uncond(masked_seq, tokenizer, model: MembraneDiffusion):
|
| 34 |
generator = UnconditionalSampler(tokenizer, model) # initialize the generator object
|
| 35 |
xt = tokenizer(masked_seq, return_tensors='pt')['input_ids'].to(model.device)
|
| 36 |
denoised_tokens = generator.sample_unconditional(xt, config.sampling.n_steps)[0].squeeze()
|
|
|
|
| 68 |
return output[0] #if batch_size==1 else output, og_sample, loc
|
| 69 |
|
| 70 |
|
| 71 |
+
def dplm_infill(masked_seq, tokenizer, model, device):
|
| 72 |
+
from src.lm.dplm.diffusion_module import DPLM
|
| 73 |
+
from src.lm.dplm.unconditional_sampler import UnconditionalSampler as DPLMUnconditionalSampler
|
| 74 |
+
|
| 75 |
generator = DPLMUnconditionalSampler(tokenizer, model)
|
| 76 |
xt = tokenizer(masked_seq, return_tensors='pt')['input_ids'].to(model.device)
|
| 77 |
denoised_tokens = generator.sample_unconditional(xt, config.sampling.n_steps)[0].squeeze()
|
|
|
|
| 114 |
with torch.no_grad():
|
| 115 |
if model_type == 'esm':
|
| 116 |
loss = model(masked_input, labels=labels).loss.item()
|
| 117 |
+
elif model_type == 'diffusion':
|
| 118 |
logits = model.forward(masked_input, attention_mask=attn_mask)
|
| 119 |
loss = F.cross_entropy(
|
| 120 |
logits.view(-1, logits.size(-1)),
|
|
|
|
| 146 |
return tot_score / len(indices) if indices else 0
|
| 147 |
|
| 148 |
|
| 149 |
+
# define TM enrichment dict
|
| 150 |
+
TM_ENRICHMENT = {
|
| 151 |
+
'A': -0.06647, 'C': 0.312396, 'D': -1.52276, 'E': -1.7743, 'F': 0.800983,
|
| 152 |
+
'G': 0.756636, 'H': -0.44366, 'I': 0.551521, 'K': -1.56399, 'L': 0.299315,
|
| 153 |
+
'M': 0.279242, 'N': -0.54795, 'P': 0.180039, 'Q': -1.16639, 'R': -1.17808,
|
| 154 |
+
'S': 0.110367, 'T': 0.228464, 'V': 0.510702, 'W': 0.562891, 'Y': 0.084097
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
def calc_tm_enrich(og_seq, gen_seq, indices):
|
| 158 |
+
def _score(seq):
|
| 159 |
+
chars = [seq[i] for i in indices if i < len(seq)] if indices else list(seq)
|
| 160 |
+
vals = [TM_ENRICHMENT.get(aa.upper()) for aa in chars]
|
| 161 |
+
vals = [v for v in vals if v is not None]
|
| 162 |
+
return float(np.mean(vals)) if vals else float('nan')
|
| 163 |
+
|
| 164 |
+
og_score = _score(og_seq)
|
| 165 |
+
gen_score = _score(gen_seq)
|
| 166 |
+
delta_score = gen_score - og_score
|
| 167 |
+
return og_score, gen_score, delta_score
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
|
| 171 |
def calc_cos_sim(original_sequence, generated_sequence, tokenizer, esm_model, device):
|
| 172 |
og_embeddings = get_latents(esm_model, tokenizer, original_sequence.upper(), device)
|
| 173 |
new_embeddings = get_latents(esm_model, tokenizer, generated_sequence, device)
|