Upload 83 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- README.md +213 -0
- configs/config_gfp.yaml +75 -0
- configs/config_selfies.yaml +57 -0
- data/.DS_Store +0 -0
- data/selfies/.DS_Store +0 -0
- data/selfies/28k_mimetics/dataset_dict.json +1 -0
- data/selfies/28k_mimetics/test/data-00000-of-00001.arrow +3 -0
- data/selfies/28k_mimetics/test/dataset_info.json +36 -0
- data/selfies/28k_mimetics/test/state.json +13 -0
- data/selfies/28k_mimetics/tokenizer/tokenizer_config.json +10 -0
- data/selfies/28k_mimetics/tokenizer/vocab.json +46 -0
- data/selfies/28k_mimetics/train/data-00000-of-00001.arrow +3 -0
- data/selfies/28k_mimetics/train/dataset_info.json +36 -0
- data/selfies/28k_mimetics/train/state.json +13 -0
- data/selfies/28k_mimetics/validation/data-00000-of-00001.arrow +3 -0
- data/selfies/28k_mimetics/validation/dataset_info.json +36 -0
- data/selfies/28k_mimetics/validation/state.json +13 -0
- evaluate.py +39 -0
- flow_matching/.DS_Store +0 -0
- flow_matching/__init__.py +7 -0
- flow_matching/loss/.DS_Store +0 -0
- flow_matching/loss/__init__.py +14 -0
- flow_matching/loss/editflows_loss.py +320 -0
- flow_matching/loss/generalized_loss.py +80 -0
- flow_matching/path/.DS_Store +0 -0
- flow_matching/path/__init__.py +24 -0
- flow_matching/path/affine.py +260 -0
- flow_matching/path/editflows_adapter.py +85 -0
- flow_matching/path/geodesic.py +100 -0
- flow_matching/path/mixture.py +117 -0
- flow_matching/path/path.py +64 -0
- flow_matching/path/path_sample.py +53 -0
- flow_matching/path/scheduler/.DS_Store +0 -0
- flow_matching/path/scheduler/__init__.py +29 -0
- flow_matching/path/scheduler/schedule_transform.py +148 -0
- flow_matching/path/scheduler/scheduler.py +199 -0
- flow_matching/solver/.DS_Store +0 -0
- flow_matching/solver/__init__.py +20 -0
- flow_matching/solver/discrete_solver.py +260 -0
- flow_matching/solver/editflows_solver.py +168 -0
- flow_matching/solver/ode_solver.py +197 -0
- flow_matching/solver/riemannian_ode_solver.py +261 -0
- flow_matching/solver/solver.py +17 -0
- flow_matching/solver/utils.py +19 -0
- flow_matching/utils/.DS_Store +0 -0
- flow_matching/utils/__init__.py +17 -0
- flow_matching/utils/categorical_sampler.py +23 -0
- flow_matching/utils/manifolds/.DS_Store +0 -0
- flow_matching/utils/manifolds/__init__.py +18 -0
- flow_matching/utils/manifolds/manifold.py +93 -0
README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# pCoMole
|
| 2 |
+
|
| 3 |
+
**pCoMole** (Pareto-Constrained Molecule editing) steers a pretrained Edit Flow toward user-specified objectives while enforcing hard terminal constraints. This repository is a minimal, clone-and-run package for:
|
| 4 |
+
|
| 5 |
+
1. Training, evaluating, and sampling **Edit Flows**
|
| 6 |
+
2. Running **pCoMole** multi-objective editing on **GFP** and **peptidomimetics**
|
| 7 |
+
|
| 8 |
+
Paper: *pCoMol: Pareto-Constrained Molecule Editing with Discrete Flows*
|
| 9 |
+
|
| 10 |
+
The original working tree is left untouched. This folder is the public-facing copy.
|
| 11 |
+
|
| 12 |
+
## Repository layout
|
| 13 |
+
|
| 14 |
+
```text
|
| 15 |
+
pCoMole/
|
| 16 |
+
train.py / evaluate.py # Edit Flow train / val-loss eval
|
| 17 |
+
configs/ # GFP (protein) and SELFIES configs
|
| 18 |
+
model/ logic/ flow_matching/ # Edit Flow implementation
|
| 19 |
+
smiles_tokenizer/ # SMILES SPE + SELFIES vocab
|
| 20 |
+
data/selfies/28k_mimetics/ # shipped SELFIES training split
|
| 21 |
+
gfp/ # GFP generation + pCoMole
|
| 22 |
+
pcomole.py # official GFP editor
|
| 23 |
+
generate.py
|
| 24 |
+
ckpt/last_2.ckpt
|
| 25 |
+
classifier_ckpt/best.pt
|
| 26 |
+
FPredX/ # excitation / brightness / emission models
|
| 27 |
+
peptidomimetics/ # peptidomimetic generation + pCoMole
|
| 28 |
+
pcomole.py
|
| 29 |
+
generate.py
|
| 30 |
+
objectives.py # PeptiVerse + Admetica/DeepDTAGen mix
|
| 31 |
+
ckpt/SELFIES_EditFlows.ckpt
|
| 32 |
+
ckpt/SMILES_BindEvaluator.ckpt
|
| 33 |
+
ckpt/admetica/ # LD50, solubility, Caco-2, half-life
|
| 34 |
+
ckpt/deepdtagen/
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
## 1. Environment
|
| 38 |
+
|
| 39 |
+
A CUDA GPU is strongly recommended.
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
git clone <this-repo-url> pCoMole
|
| 43 |
+
cd pCoMole
|
| 44 |
+
python -m venv .venv
|
| 45 |
+
source .venv/bin/activate
|
| 46 |
+
pip install -r requirements.txt
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
HuggingFace model downloads happen on first use (`facebook/esm2_t33_650M_UR50D`, `aaronfeller/PeptideCLM-23M-all`, ChemBERTa).
|
| 50 |
+
|
| 51 |
+
### PeptiVerse (required for peptidomimetic pCoMole)
|
| 52 |
+
|
| 53 |
+
Peptidomimetic property oracles use the latest [PeptiVerse](https://huggingface.co/ChatterjeeLab/PeptiVerse) SMILES predictors. Clone it **next to** this repo, or set `PEPTIVERSE_ROOT`:
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
# recommended: sibling of pCoMole
|
| 57 |
+
cd ..
|
| 58 |
+
git clone https://huggingface.co/ChatterjeeLab/PeptiVerse
|
| 59 |
+
cd PeptiVerse
|
| 60 |
+
pip install -r requirements.txt
|
| 61 |
+
# follow PeptiVerse README to download model weights
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
pCoMole looks for PeptiVerse in this order:
|
| 65 |
+
|
| 66 |
+
1. `$PEPTIVERSE_ROOT`
|
| 67 |
+
2. `../PeptiVerse` relative to this repository
|
| 68 |
+
|
| 69 |
+
PeptiVerse scores the peptide-like side of each candidate. Admetica + DeepDTAGen still score the small-molecule side, and the original peptide-likeness mix logic is unchanged.
|
| 70 |
+
|
| 71 |
+
### MAFFT (required for GFP pCoMole)
|
| 72 |
+
|
| 73 |
+
GFP excitation, brightness, and emission oracles run through FPredX and need [MAFFT](https://mafft.cbrc.jp/alignment/software/) on your `PATH`.
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
# conda
|
| 77 |
+
conda install -c bioconda mafft
|
| 78 |
+
|
| 79 |
+
# or point to a local binary
|
| 80 |
+
export MAFFT_PATH=/path/to/mafft
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
Confirm with `mafft --version` before running GFP editing.
|
| 84 |
+
|
| 85 |
+
## 2. Train, evaluate, and sample Edit Flows
|
| 86 |
+
|
| 87 |
+
Run every command from the **repository root**.
|
| 88 |
+
|
| 89 |
+
### SELFIES peptidomimetic Edit Flow
|
| 90 |
+
|
| 91 |
+
Training data is shipped at `data/selfies/28k_mimetics`.
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
# train
|
| 95 |
+
python train.py --config configs/config_selfies.yaml
|
| 96 |
+
# optional: python train.py --config configs/config_selfies.yaml --wandb
|
| 97 |
+
|
| 98 |
+
# evaluate a checkpoint on the validation split
|
| 99 |
+
python evaluate.py \
|
| 100 |
+
--config configs/config_selfies.yaml \
|
| 101 |
+
--ckpt peptidomimetics/ckpt/SELFIES_EditFlows.ckpt
|
| 102 |
+
|
| 103 |
+
# unconditional / seeded generation
|
| 104 |
+
python peptidomimetics/generate.py \
|
| 105 |
+
--config configs/config_selfies.yaml \
|
| 106 |
+
--ckpt peptidomimetics/ckpt/SELFIES_EditFlows.ckpt \
|
| 107 |
+
--input 'CSCC[C@H](NC(=O)[C@@H]1CCCN1C(=O)[C@H](Cc1ccc(O)cc1)NC(=O)[C@H](CCCNC(=N)N)NC(=O)[C@H](CO)NC(=O)[C@H](Cc1ccc(O)cc1)NC(=O)[C@@H]1CCCN1C(=O)[C@@H]1CCCN1C(=O)[C@@H]1CCCN1C(=O)[C@@H]1CCCN1C(=O)[C@@H](N)CO)C(=O)N[C@@H](CC(=O)O)C(=O)O' \
|
| 108 |
+
--num_steps 30 \
|
| 109 |
+
--num_samples 2 \
|
| 110 |
+
--output_csv outputs/peptidomimetic_unconditional.csv
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
Or: `bash peptidomimetics/scripts/train.sh` and `bash peptidomimetics/scripts/generate.sh`.
|
| 114 |
+
|
| 115 |
+
### GFP protein Edit Flow
|
| 116 |
+
|
| 117 |
+
A trained GFP checkpoint is shipped at `gfp/ckpt/last_2.ckpt`. The original GFP training arrows are **not** included. To retrain, put a HuggingFace `load_from_disk` dataset under `data/gfp/{train,validation}` (or edit `configs/config_gfp.yaml`).
|
| 118 |
+
|
| 119 |
+
```bash
|
| 120 |
+
python evaluate.py --config configs/config_gfp.yaml --ckpt gfp/ckpt/last_2.ckpt
|
| 121 |
+
|
| 122 |
+
python gfp/generate.py \
|
| 123 |
+
--config configs/config_gfp.yaml \
|
| 124 |
+
--ckpt gfp/ckpt/last_2.ckpt \
|
| 125 |
+
--input 'MSSGALLFHGKIPYVVEMEGNVDGHTFSIRGKGYGDASVGKVDAQFICTTGDVPVPWSTLVTTLTYGAQCFAKYGPELKDFYKSCMPDGYVQERTITFEGDGNFKTRAEVTFENGSVYNRVKLNGQGFKKDGHVLGKNLEFNFTPHCLYIWGDQANHGLKSAFKICHEITGSKGDFIVADHTQMNTPIGGGPVHVPEYHHMSYHVKLSKDVTDHRDNMSLKETVRAVDCRKTYDFDAGSGDTS' \
|
| 126 |
+
--num_steps 10 \
|
| 127 |
+
--num_samples 8 \
|
| 128 |
+
--output_csv outputs/gfp_unconditional.csv
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
## 3. pCoMole multi-objective editing
|
| 132 |
+
|
| 133 |
+
### GFP
|
| 134 |
+
|
| 135 |
+
Official editor: `gfp/pcomole.py` (length + excitation + brightness, with GFP-classifier and emission constraints).
|
| 136 |
+
|
| 137 |
+
```bash
|
| 138 |
+
bash gfp/scripts/pcomole.sh
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
Equivalent command:
|
| 142 |
+
|
| 143 |
+
```bash
|
| 144 |
+
python gfp/pcomole.py \
|
| 145 |
+
--root_dir gfp/FPredX \
|
| 146 |
+
--config configs/config_gfp.yaml \
|
| 147 |
+
--ckpt gfp/ckpt/last_2.ckpt \
|
| 148 |
+
--num_steps 10 \
|
| 149 |
+
--num_candidates 50 \
|
| 150 |
+
--num_rollouts 10 \
|
| 151 |
+
--objective_weights 3 1 1 \
|
| 152 |
+
--output_file outputs/gfp_length_excitation_brightness.csv \
|
| 153 |
+
--input 'MSSGALLFHGKIPYVVEMEGNVDGHTFSIRGKGYGDASVGKVDAQFICTTGDVPVPWSTLVTTLTYGAQCFAKYGPELKDFYKSCMPDGYVQERTITFEGDGNFKTRAEVTFENGSVYNRVKLNGQGFKKDGHVLGKNLEFNFTPHCLYIWGDQANHGLKSAFKICHEITGSKGDFIVADHTQMNTPIGGGPVHVPEYHHMSYHVKLSKDVTDHRDNMSLKETVRAVDCRKTYDFDAGSGDTS'
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
If the output filename contains `length_excitation_brightness`, `length_brightness`, `length_excitation`, or `length`, the matching objective subset is used. Any other name uses all three objectives.
|
| 157 |
+
|
| 158 |
+
### Peptidomimetics
|
| 159 |
+
|
| 160 |
+
Official editor: `peptidomimetics/pcomole.py`.
|
| 161 |
+
|
| 162 |
+
Objectives (7 scores when `--specificity` is on):
|
| 163 |
+
|
| 164 |
+
1. non-toxicity
|
| 165 |
+
2. solubility
|
| 166 |
+
3. permeability
|
| 167 |
+
4. half-life
|
| 168 |
+
5. affinity
|
| 169 |
+
6. motif
|
| 170 |
+
7. specificity
|
| 171 |
+
|
| 172 |
+
PeptiVerse provides the peptide-side scores. Admetica + DeepDTAGen provide the small-molecule side. The two are mixed by peptide-likeness, same as the paper code. Motif / specificity still use the shipped BindEvaluator checkpoint.
|
| 173 |
+
|
| 174 |
+
```bash
|
| 175 |
+
# after PeptiVerse is cloned and its weights are downloaded
|
| 176 |
+
bash peptidomimetics/scripts/pcomole.sh
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
`--target` and `--motifs` are required for affinity and motif oracles. `--specificity` adds the seventh objective.
|
| 180 |
+
|
| 181 |
+
## 4. Included checkpoints
|
| 182 |
+
|
| 183 |
+
| Path | Role |
|
| 184 |
+
|---|---|
|
| 185 |
+
| `gfp/ckpt/last_2.ckpt` | GFP Edit Flow |
|
| 186 |
+
| `gfp/classifier_ckpt/best.pt` | GFP hard constraint |
|
| 187 |
+
| `gfp/FPredX/{ex,bright,em}_model` | FPredX property models |
|
| 188 |
+
| `peptidomimetics/ckpt/SELFIES_EditFlows.ckpt` | peptidomimetic Edit Flow |
|
| 189 |
+
| `peptidomimetics/ckpt/SMILES_BindEvaluator.ckpt` | motif / specificity |
|
| 190 |
+
| `peptidomimetics/ckpt/admetica/*.ckpt` | Admetica blending oracles |
|
| 191 |
+
| `peptidomimetics/ckpt/deepdtagen/` | DeepDTAGen affinity |
|
| 192 |
+
|
| 193 |
+
Together these are about 10 GB. HuggingFace uploads should use Git LFS for the `.ckpt` / `.pt` / `.pth` files.
|
| 194 |
+
|
| 195 |
+
## 5. Environment variables
|
| 196 |
+
|
| 197 |
+
| Variable | Purpose |
|
| 198 |
+
|---|---|
|
| 199 |
+
| `PEPTIVERSE_ROOT` | PeptiVerse repo root (manifest + `training_classifiers/`) |
|
| 200 |
+
| `MAFFT_PATH` | MAFFT binary, if it is not on `PATH` |
|
| 201 |
+
| `GFP_CLASSIFIER_CKPT` | optional override for the GFP classifier |
|
| 202 |
+
|
| 203 |
+
## 6. Notes
|
| 204 |
+
|
| 205 |
+
- Run scripts from the repo root, or use the wrappers under `*/scripts/`.
|
| 206 |
+
- GFP FPredX will fail immediately if `mafft` is missing; that is expected.
|
| 207 |
+
- Peptidomimetic pCoMole will fail at oracle init if PeptiVerse is missing or its weights were not downloaded.
|
| 208 |
+
- Cas9 is not part of this release.
|
| 209 |
+
- Training writes to `outputs/` by default.
|
| 210 |
+
|
| 211 |
+
## Citation
|
| 212 |
+
|
| 213 |
+
If you use this code, please cite the pCoMole paper and [PeptiVerse](https://www.biorxiv.org/content/10.64898/2025.12.31.697180).
|
configs/config_gfp.yaml
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
task: protein
|
| 2 |
+
|
| 3 |
+
compute:
|
| 4 |
+
ngpus: 1
|
| 5 |
+
nodes: 1
|
| 6 |
+
|
| 7 |
+
logging:
|
| 8 |
+
enable_wandb: False
|
| 9 |
+
project: pCoMole
|
| 10 |
+
|
| 11 |
+
data:
|
| 12 |
+
# GFP training arrows are not shipped. Point these at your own
|
| 13 |
+
# HuggingFace load_from_disk() dataset if you want to retrain.
|
| 14 |
+
train_path: data/gfp/train
|
| 15 |
+
val_path: data/gfp/validation
|
| 16 |
+
num_workers: 1
|
| 17 |
+
|
| 18 |
+
training:
|
| 19 |
+
batch_size: None
|
| 20 |
+
snapshot: 100
|
| 21 |
+
eval_freq: 1000
|
| 22 |
+
perplexity_freq: 100
|
| 23 |
+
seed: 42
|
| 24 |
+
grad_accumulation_steps: 2
|
| 25 |
+
loc_prop_path: False
|
| 26 |
+
lam_prop: 1
|
| 27 |
+
reparameterize: True
|
| 28 |
+
gamma_rate: 1
|
| 29 |
+
gamma_edit: 1
|
| 30 |
+
use_aux_ce: False
|
| 31 |
+
aux_ce_weight: 1.0
|
| 32 |
+
no_training: False
|
| 33 |
+
|
| 34 |
+
eval:
|
| 35 |
+
batch_size: None
|
| 36 |
+
sample_batch_size: 4
|
| 37 |
+
perplexity: False
|
| 38 |
+
perplexity_batch_size: 4
|
| 39 |
+
|
| 40 |
+
optim:
|
| 41 |
+
weight_decay: 0.05
|
| 42 |
+
optimizer: AdamW
|
| 43 |
+
lr: 1.0e-6
|
| 44 |
+
beta1: 0.9
|
| 45 |
+
beta2: 0.95
|
| 46 |
+
eps: 1.0e-8
|
| 47 |
+
grad_clip: 1.0
|
| 48 |
+
eta_min_ratio: 0.1
|
| 49 |
+
fused: false
|
| 50 |
+
n_epochs: 1000
|
| 51 |
+
warmup_ratio: 0.1
|
| 52 |
+
|
| 53 |
+
flow:
|
| 54 |
+
source_distribution: uniform
|
| 55 |
+
loss_function: editflows
|
| 56 |
+
exponent: 2.0
|
| 57 |
+
scheduler_type: polynomial
|
| 58 |
+
sampling_steps: 256
|
| 59 |
+
eps_id: -1
|
| 60 |
+
|
| 61 |
+
model:
|
| 62 |
+
d_model: 768
|
| 63 |
+
max_len: 2048
|
| 64 |
+
n_layers: 12
|
| 65 |
+
n_heads: 12
|
| 66 |
+
dropout: 0.1
|
| 67 |
+
attn_dropout: 0.0
|
| 68 |
+
proj_dropout: 0.0
|
| 69 |
+
compile: False
|
| 70 |
+
esm_model_name: facebook/esm2_t33_650M_UR50D
|
| 71 |
+
freeze_esm: true
|
| 72 |
+
p_optimal: 0
|
| 73 |
+
scale_size: 0.5
|
| 74 |
+
|
| 75 |
+
work_dir: outputs/gfp
|
configs/config_selfies.yaml
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
task: selfies
|
| 2 |
+
|
| 3 |
+
compute:
|
| 4 |
+
ngpus: 1
|
| 5 |
+
nodes: 1
|
| 6 |
+
|
| 7 |
+
data:
|
| 8 |
+
train_path: data/selfies/28k_mimetics/train
|
| 9 |
+
val_path: data/selfies/28k_mimetics/validation
|
| 10 |
+
num_workers: 4
|
| 11 |
+
|
| 12 |
+
training:
|
| 13 |
+
batch_size: 4
|
| 14 |
+
snapshot: 100
|
| 15 |
+
eval_freq: 1000
|
| 16 |
+
perplexity_freq: 100
|
| 17 |
+
seed: 42
|
| 18 |
+
|
| 19 |
+
eval:
|
| 20 |
+
batch_size: 4
|
| 21 |
+
sample_batch_size: 4
|
| 22 |
+
perplexity: False
|
| 23 |
+
perplexity_batch_size: 4
|
| 24 |
+
|
| 25 |
+
optim:
|
| 26 |
+
weight_decay: 0.03
|
| 27 |
+
optimizer: AdamW
|
| 28 |
+
lr: 3.0e-4
|
| 29 |
+
beta1: 0.9
|
| 30 |
+
beta2: 0.95
|
| 31 |
+
eps: 1.0e-8
|
| 32 |
+
grad_clip: 1.0
|
| 33 |
+
eta_min_ratio: 0.1
|
| 34 |
+
fused: false
|
| 35 |
+
n_epochs: 100
|
| 36 |
+
warmup_ratio: 0.1
|
| 37 |
+
|
| 38 |
+
flow:
|
| 39 |
+
source_distribution: uniform
|
| 40 |
+
loss_function: editflows_reparam
|
| 41 |
+
exponent: 2.0
|
| 42 |
+
scheduler_type: polynomial
|
| 43 |
+
sampling_steps: 256
|
| 44 |
+
eps_id: -1
|
| 45 |
+
|
| 46 |
+
model:
|
| 47 |
+
hidden_size: 768
|
| 48 |
+
cond_dim: 128
|
| 49 |
+
length: 2048
|
| 50 |
+
n_blocks: 8
|
| 51 |
+
n_heads: 12
|
| 52 |
+
dropout: 0.1
|
| 53 |
+
compile: False
|
| 54 |
+
p_optimal: 0
|
| 55 |
+
scale_size: 1.0
|
| 56 |
+
|
| 57 |
+
work_dir: outputs/selfies
|
data/.DS_Store
ADDED
|
Binary file (8.2 kB). View file
|
|
|
data/selfies/.DS_Store
ADDED
|
Binary file (8.2 kB). View file
|
|
|
data/selfies/28k_mimetics/dataset_dict.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"splits": ["train", "validation", "test"]}
|
data/selfies/28k_mimetics/test/data-00000-of-00001.arrow
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d709f3386113ebc6abe9400950274e2e41aaf7063b31e67d6ba15f3239462251
|
| 3 |
+
size 786928
|
data/selfies/28k_mimetics/test/dataset_info.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"citation": "",
|
| 3 |
+
"description": "",
|
| 4 |
+
"features": {
|
| 5 |
+
"input_ids": {
|
| 6 |
+
"feature": {
|
| 7 |
+
"feature": {
|
| 8 |
+
"dtype": "int32",
|
| 9 |
+
"_type": "Value"
|
| 10 |
+
},
|
| 11 |
+
"_type": "List"
|
| 12 |
+
},
|
| 13 |
+
"_type": "List"
|
| 14 |
+
},
|
| 15 |
+
"attention_mask": {
|
| 16 |
+
"feature": {
|
| 17 |
+
"feature": {
|
| 18 |
+
"dtype": "int8",
|
| 19 |
+
"_type": "Value"
|
| 20 |
+
},
|
| 21 |
+
"_type": "List"
|
| 22 |
+
},
|
| 23 |
+
"_type": "List"
|
| 24 |
+
},
|
| 25 |
+
"seq_length": {
|
| 26 |
+
"dtype": "int64",
|
| 27 |
+
"_type": "Value"
|
| 28 |
+
},
|
| 29 |
+
"batch_size": {
|
| 30 |
+
"dtype": "int64",
|
| 31 |
+
"_type": "Value"
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
"homepage": "",
|
| 35 |
+
"license": ""
|
| 36 |
+
}
|
data/selfies/28k_mimetics/test/state.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_data_files": [
|
| 3 |
+
{
|
| 4 |
+
"filename": "data-00000-of-00001.arrow"
|
| 5 |
+
}
|
| 6 |
+
],
|
| 7 |
+
"_fingerprint": "98db6912c1e59254",
|
| 8 |
+
"_format_columns": null,
|
| 9 |
+
"_format_kwargs": {},
|
| 10 |
+
"_format_type": null,
|
| 11 |
+
"_output_all_columns": false,
|
| 12 |
+
"_split": null
|
| 13 |
+
}
|
data/selfies/28k_mimetics/tokenizer/tokenizer_config.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"type": "selfies-wordlevel",
|
| 3 |
+
"version": "2.1.1",
|
| 4 |
+
"special_tokens": {
|
| 5 |
+
"pad": "<pad>",
|
| 6 |
+
"bos": "<bos>",
|
| 7 |
+
"eos": "<eos>"
|
| 8 |
+
},
|
| 9 |
+
"semantic_constraints": null
|
| 10 |
+
}
|
data/selfies/28k_mimetics/tokenizer/vocab.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"<bos>": 1,
|
| 3 |
+
"<eos>": 2,
|
| 4 |
+
"<pad>": 0,
|
| 5 |
+
"[#Branch1]": 3,
|
| 6 |
+
"[#Branch2]": 4,
|
| 7 |
+
"[#C]": 5,
|
| 8 |
+
"[#N]": 6,
|
| 9 |
+
"[/-Ring1]": 7,
|
| 10 |
+
"[/C]": 8,
|
| 11 |
+
"[/N]": 9,
|
| 12 |
+
"[=Branch1]": 10,
|
| 13 |
+
"[=Branch2]": 11,
|
| 14 |
+
"[=C]": 12,
|
| 15 |
+
"[=N]": 13,
|
| 16 |
+
"[=O]": 14,
|
| 17 |
+
"[=Ring1]": 15,
|
| 18 |
+
"[=Ring2]": 16,
|
| 19 |
+
"[=S]": 17,
|
| 20 |
+
"[Br]": 18,
|
| 21 |
+
"[Branch1]": 19,
|
| 22 |
+
"[Branch2]": 20,
|
| 23 |
+
"[C@@H1]": 21,
|
| 24 |
+
"[C@@]": 22,
|
| 25 |
+
"[C@H1]": 23,
|
| 26 |
+
"[C@]": 24,
|
| 27 |
+
"[CH1-1]": 25,
|
| 28 |
+
"[C]": 26,
|
| 29 |
+
"[Cl]": 27,
|
| 30 |
+
"[F]": 28,
|
| 31 |
+
"[I]": 29,
|
| 32 |
+
"[N+1]": 30,
|
| 33 |
+
"[N-1]": 31,
|
| 34 |
+
"[NH1]": 32,
|
| 35 |
+
"[N]": 33,
|
| 36 |
+
"[O-1]": 34,
|
| 37 |
+
"[O]": 35,
|
| 38 |
+
"[P]": 36,
|
| 39 |
+
"[Ring1]": 37,
|
| 40 |
+
"[Ring2]": 38,
|
| 41 |
+
"[S]": 39,
|
| 42 |
+
"[\\-Ring1]": 40,
|
| 43 |
+
"[\\-Ring2]": 41,
|
| 44 |
+
"[\\C]": 42,
|
| 45 |
+
"[\\N]": 43
|
| 46 |
+
}
|
data/selfies/28k_mimetics/train/data-00000-of-00001.arrow
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4d7e3d408b36d7c8a75b0d36f20d09b744c1c845961401c72a4023a9a6ffa3f6
|
| 3 |
+
size 6084456
|
data/selfies/28k_mimetics/train/dataset_info.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"citation": "",
|
| 3 |
+
"description": "",
|
| 4 |
+
"features": {
|
| 5 |
+
"input_ids": {
|
| 6 |
+
"feature": {
|
| 7 |
+
"feature": {
|
| 8 |
+
"dtype": "int32",
|
| 9 |
+
"_type": "Value"
|
| 10 |
+
},
|
| 11 |
+
"_type": "List"
|
| 12 |
+
},
|
| 13 |
+
"_type": "List"
|
| 14 |
+
},
|
| 15 |
+
"attention_mask": {
|
| 16 |
+
"feature": {
|
| 17 |
+
"feature": {
|
| 18 |
+
"dtype": "int8",
|
| 19 |
+
"_type": "Value"
|
| 20 |
+
},
|
| 21 |
+
"_type": "List"
|
| 22 |
+
},
|
| 23 |
+
"_type": "List"
|
| 24 |
+
},
|
| 25 |
+
"seq_length": {
|
| 26 |
+
"dtype": "int64",
|
| 27 |
+
"_type": "Value"
|
| 28 |
+
},
|
| 29 |
+
"batch_size": {
|
| 30 |
+
"dtype": "int64",
|
| 31 |
+
"_type": "Value"
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
"homepage": "",
|
| 35 |
+
"license": ""
|
| 36 |
+
}
|
data/selfies/28k_mimetics/train/state.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_data_files": [
|
| 3 |
+
{
|
| 4 |
+
"filename": "data-00000-of-00001.arrow"
|
| 5 |
+
}
|
| 6 |
+
],
|
| 7 |
+
"_fingerprint": "58b5e20848e4941d",
|
| 8 |
+
"_format_columns": null,
|
| 9 |
+
"_format_kwargs": {},
|
| 10 |
+
"_format_type": null,
|
| 11 |
+
"_output_all_columns": false,
|
| 12 |
+
"_split": null
|
| 13 |
+
}
|
data/selfies/28k_mimetics/validation/data-00000-of-00001.arrow
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6667b7a12a440b50d996c8523813df0965a718e1a467ef2162fdea65d9b972cb
|
| 3 |
+
size 778640
|
data/selfies/28k_mimetics/validation/dataset_info.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"citation": "",
|
| 3 |
+
"description": "",
|
| 4 |
+
"features": {
|
| 5 |
+
"input_ids": {
|
| 6 |
+
"feature": {
|
| 7 |
+
"feature": {
|
| 8 |
+
"dtype": "int32",
|
| 9 |
+
"_type": "Value"
|
| 10 |
+
},
|
| 11 |
+
"_type": "List"
|
| 12 |
+
},
|
| 13 |
+
"_type": "List"
|
| 14 |
+
},
|
| 15 |
+
"attention_mask": {
|
| 16 |
+
"feature": {
|
| 17 |
+
"feature": {
|
| 18 |
+
"dtype": "int8",
|
| 19 |
+
"_type": "Value"
|
| 20 |
+
},
|
| 21 |
+
"_type": "List"
|
| 22 |
+
},
|
| 23 |
+
"_type": "List"
|
| 24 |
+
},
|
| 25 |
+
"seq_length": {
|
| 26 |
+
"dtype": "int64",
|
| 27 |
+
"_type": "Value"
|
| 28 |
+
},
|
| 29 |
+
"batch_size": {
|
| 30 |
+
"dtype": "int64",
|
| 31 |
+
"_type": "Value"
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
"homepage": "",
|
| 35 |
+
"license": ""
|
| 36 |
+
}
|
data/selfies/28k_mimetics/validation/state.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_data_files": [
|
| 3 |
+
{
|
| 4 |
+
"filename": "data-00000-of-00001.arrow"
|
| 5 |
+
}
|
| 6 |
+
],
|
| 7 |
+
"_fingerprint": "8dc499c569440f6b",
|
| 8 |
+
"_format_columns": null,
|
| 9 |
+
"_format_kwargs": {},
|
| 10 |
+
"_format_type": null,
|
| 11 |
+
"_output_all_columns": false,
|
| 12 |
+
"_split": null
|
| 13 |
+
}
|
evaluate.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import pytorch_lightning as pl
|
| 5 |
+
|
| 6 |
+
from paths import add_repo_to_sys_path, resolve_path
|
| 7 |
+
from train import build_dataloaders, build_editflow, load_config
|
| 8 |
+
|
| 9 |
+
add_repo_to_sys_path()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def main():
|
| 13 |
+
parser = argparse.ArgumentParser(description="Evaluate an Edit Flow checkpoint on the validation split")
|
| 14 |
+
parser.add_argument("--config", type=str, required=True)
|
| 15 |
+
parser.add_argument("--ckpt", type=str, required=True)
|
| 16 |
+
args = parser.parse_args()
|
| 17 |
+
|
| 18 |
+
cfg = load_config(args.config)
|
| 19 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 20 |
+
editflow, _, _, _, _, _, _ = build_editflow(cfg, device=device)
|
| 21 |
+
|
| 22 |
+
ckpt = torch.load(resolve_path(args.ckpt), map_location=device, weights_only=False)
|
| 23 |
+
state = ckpt["state_dict"] if isinstance(ckpt, dict) and "state_dict" in ckpt else ckpt
|
| 24 |
+
editflow.load_state_dict(state, strict=False)
|
| 25 |
+
editflow.eval()
|
| 26 |
+
|
| 27 |
+
_, val_dataloader = build_dataloaders(cfg)
|
| 28 |
+
trainer = pl.Trainer(
|
| 29 |
+
accelerator="gpu" if torch.cuda.is_available() else "cpu",
|
| 30 |
+
devices=1,
|
| 31 |
+
logger=False,
|
| 32 |
+
enable_checkpointing=False,
|
| 33 |
+
)
|
| 34 |
+
metrics = trainer.validate(editflow, val_dataloader, verbose=True)
|
| 35 |
+
print(metrics)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
if __name__ == "__main__":
|
| 39 |
+
main()
|
flow_matching/.DS_Store
ADDED
|
Binary file (14.3 kB). View file
|
|
|
flow_matching/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
__version__ = "1.0.10"
|
flow_matching/loss/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
flow_matching/loss/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
from .generalized_loss import MixturePathGeneralizedKL
|
| 8 |
+
from .editflows_loss import EditFlowsLoss, EditFlowsLossReParam
|
| 9 |
+
|
| 10 |
+
__all__ = [
|
| 11 |
+
"MixturePathGeneralizedKL",
|
| 12 |
+
"EditFlowsLoss",
|
| 13 |
+
"EditFlowsLossReParam"
|
| 14 |
+
]
|
flow_matching/loss/editflows_loss.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
|
| 4 |
+
from typing import List, Optional, Sequence
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
from torch import Tensor
|
| 8 |
+
from torch.nn.modules.loss import _Loss
|
| 9 |
+
|
| 10 |
+
# If you kept the adapter around only for scheduler access you can drop it entirely,
|
| 11 |
+
# because we pass precomputed_weight from training.step. Keeping it is harmless though.
|
| 12 |
+
class EditFlowsLoss(_Loss):
|
| 13 |
+
"""
|
| 14 |
+
Edit Flows loss (Eq. 23), ragged version:
|
| 15 |
+
L_i = (sum_j λ_ins[i][j] + sum_j λ_del[i][j] + sum_j λ_sub[i][j])
|
| 16 |
+
- w_i * ( sum_{ins events e} [log λ_ins[i][slot_e] + log Q_ins[i](y_e)]
|
| 17 |
+
+ sum_{del j} [log λ_del[i][j]]
|
| 18 |
+
+ sum_{sub j} [log λ_sub[i][j] + log Q_sub[i](y_j)] )
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def __init__(self, reduction: str = "mean") -> None:
|
| 22 |
+
super().__init__(None, None, reduction)
|
| 23 |
+
|
| 24 |
+
def forward(
|
| 25 |
+
self,
|
| 26 |
+
lam_ins: torch.Tensor, # (B, L)
|
| 27 |
+
logits_ins: torch.Tensor, # (B, L, V)
|
| 28 |
+
lam_del: torch.Tensor, # (B, L)
|
| 29 |
+
lam_sub: torch.Tensor, # (B, L)
|
| 30 |
+
logits_sub: torch.Tensor, # (B, L, V)
|
| 31 |
+
z_t: torch.Tensor, # (B, N) aligned, with eps_id
|
| 32 |
+
z_1: torch.Tensor, # (B, N) aligned target, with eps_id
|
| 33 |
+
x_t: torch.Tensor, # (B, L)
|
| 34 |
+
valid_mask: torch.Tensor, # (B, L) bool, False==padding, True==valid
|
| 35 |
+
precomputed_weight: torch.Tensor, # (B,) or () = kappa_dot/(1-kappa)
|
| 36 |
+
eps_id: int,
|
| 37 |
+
bos_id: int,
|
| 38 |
+
eos_id: int,
|
| 39 |
+
) -> torch.Tensor:
|
| 40 |
+
"""
|
| 41 |
+
Implements Eq. 23 style loss for Edit Flows.
|
| 42 |
+
|
| 43 |
+
We:
|
| 44 |
+
1) penalize total outgoing rate on x_t
|
| 45 |
+
2) for every column in (z_t, z_1) that still differs, we map it to an x_t edit
|
| 46 |
+
and add - w * log u_required
|
| 47 |
+
|
| 48 |
+
BOS/EOS handling:
|
| 49 |
+
- z_t and z_1 are aligned and already contain BOS/EOS
|
| 50 |
+
- we do NOT allow edits that delete/replace BOS/EOS
|
| 51 |
+
- we also skip columns whose target token is BOS/EOS (they should already match)
|
| 52 |
+
"""
|
| 53 |
+
device = lam_ins.device
|
| 54 |
+
B, L = x_t.shape
|
| 55 |
+
_, N = z_t.shape
|
| 56 |
+
|
| 57 |
+
# 1. ----- RATE TERM -----
|
| 58 |
+
# valid_mask: True = real token, False = pad
|
| 59 |
+
valid_f = valid_mask.to(lam_ins.dtype) # (B, L) 1.0 for valid, 0.0 for pad
|
| 60 |
+
|
| 61 |
+
# total outgoing rate at each position
|
| 62 |
+
total_rate_pos = lam_ins + lam_del + lam_sub # (B, L)
|
| 63 |
+
total_rate_pos = total_rate_pos * valid_f # zero out pads
|
| 64 |
+
loss_rate = total_rate_pos.sum(dim=1) # (B,)
|
| 65 |
+
|
| 66 |
+
# 2. ----- EDIT TERM -----
|
| 67 |
+
# precompute log-softmax for tokens (better numerics)
|
| 68 |
+
logp_ins = F.log_softmax(logits_ins, dim=-1) # (B, L, V)
|
| 69 |
+
logp_sub = F.log_softmax(logits_sub, dim=-1) # (B, L, V)
|
| 70 |
+
|
| 71 |
+
# Make weight shape nice
|
| 72 |
+
if precomputed_weight.dim() == 0:
|
| 73 |
+
precomputed_weight = precomputed_weight.view(1).expand(B).to(device)
|
| 74 |
+
else:
|
| 75 |
+
precomputed_weight = precomputed_weight.to(device)
|
| 76 |
+
|
| 77 |
+
# use float for accumulation
|
| 78 |
+
loss_edit = torch.zeros(B, dtype=torch.float32, device=device)
|
| 79 |
+
|
| 80 |
+
for b in range(B):
|
| 81 |
+
# how many *valid* tokens in x_t[b]
|
| 82 |
+
valid_len = int(valid_mask[b].sum().item())
|
| 83 |
+
|
| 84 |
+
prefix_non_eps = 0 # = number of non-ε seen in z_t[b, :i]
|
| 85 |
+
|
| 86 |
+
for i in range(N):
|
| 87 |
+
zt = int(z_t[b, i].item())
|
| 88 |
+
z1 = int(z_1[b, i].item())
|
| 89 |
+
|
| 90 |
+
# map this column to an x_t position
|
| 91 |
+
if zt != eps_id:
|
| 92 |
+
# this column corresponds to x_t[b, prefix_non_eps]
|
| 93 |
+
x_pos = prefix_non_eps
|
| 94 |
+
is_token = True
|
| 95 |
+
prefix_non_eps += 1
|
| 96 |
+
else:
|
| 97 |
+
# this is a gap column, sits BETWEEN tokens
|
| 98 |
+
x_pos = prefix_non_eps
|
| 99 |
+
is_token = False
|
| 100 |
+
|
| 101 |
+
# if already matched -> no term
|
| 102 |
+
if zt == z1:
|
| 103 |
+
continue
|
| 104 |
+
|
| 105 |
+
# --- BOS/EOS guards on the target side ---
|
| 106 |
+
# if the target token is BOS or EOS, we shouldn't try to force an edit here
|
| 107 |
+
if z1 == bos_id or z1 == eos_id:
|
| 108 |
+
# skip this column; aligned BOS/EOS should already match
|
| 109 |
+
continue
|
| 110 |
+
|
| 111 |
+
# figure out which edit we need
|
| 112 |
+
# CASE 1: deletion: z_t has token, z_1 has ε
|
| 113 |
+
if is_token and (z1 == eps_id):
|
| 114 |
+
# delete token at x_pos
|
| 115 |
+
if x_pos >= valid_len:
|
| 116 |
+
# out of range (shouldn't happen if alignment & mask match)
|
| 117 |
+
raise NotImplementedError
|
| 118 |
+
|
| 119 |
+
x_token = int(x_t[b, x_pos].item())
|
| 120 |
+
# do NOT delete BOS/EOS
|
| 121 |
+
if x_token == bos_id or x_token == eos_id:
|
| 122 |
+
continue
|
| 123 |
+
|
| 124 |
+
lam = lam_del[b, x_pos].clamp_min(1e-12)
|
| 125 |
+
log_u_req = torch.log(lam)
|
| 126 |
+
|
| 127 |
+
# CASE 2: substitution: token -> different token
|
| 128 |
+
elif is_token and (z1 != eps_id) and (zt != z1):
|
| 129 |
+
if x_pos >= valid_len:
|
| 130 |
+
raise NotImplementedError
|
| 131 |
+
|
| 132 |
+
x_token = int(x_t[b, x_pos].item())
|
| 133 |
+
# do NOT substitute BOS/EOS
|
| 134 |
+
if x_token == bos_id or x_token == eos_id:
|
| 135 |
+
continue
|
| 136 |
+
|
| 137 |
+
lam = lam_sub[b, x_pos].clamp_min(1e-12)
|
| 138 |
+
logp_tok = logp_sub[b, x_pos, z1]
|
| 139 |
+
log_u_req = torch.log(lam) + logp_tok
|
| 140 |
+
|
| 141 |
+
# CASE 3: insertion: ε -> token
|
| 142 |
+
elif (not is_token) and (z1 != eps_id):
|
| 143 |
+
# insertion in the gap after token (x_pos - 1)
|
| 144 |
+
# if x_pos == 0, we insert after "BOS"/at start -> map to position 0
|
| 145 |
+
ins_pos = x_pos - 1
|
| 146 |
+
if ins_pos < 0:
|
| 147 |
+
ins_pos = 0
|
| 148 |
+
|
| 149 |
+
# clamp to last valid position if needed
|
| 150 |
+
if valid_len == 0:
|
| 151 |
+
# degenerate, but avoid -1
|
| 152 |
+
ins_pos = 0
|
| 153 |
+
elif ins_pos >= valid_len:
|
| 154 |
+
ins_pos = valid_len - 1
|
| 155 |
+
|
| 156 |
+
# also don't insert "after" EOS if ins_pos currently points to EOS
|
| 157 |
+
x_token = int(x_t[b, ins_pos].item())
|
| 158 |
+
if x_token == eos_id:
|
| 159 |
+
# simplest policy: skip this insertion supervision
|
| 160 |
+
continue
|
| 161 |
+
|
| 162 |
+
lam = lam_ins[b, ins_pos].clamp_min(1e-12)
|
| 163 |
+
logp_tok = logp_ins[b, ins_pos, z1]
|
| 164 |
+
log_u_req = torch.log(lam) + logp_tok
|
| 165 |
+
|
| 166 |
+
else:
|
| 167 |
+
# unknown pattern (shouldn't happen with proper alignment)
|
| 168 |
+
raise NotImplementedError
|
| 169 |
+
|
| 170 |
+
w = precomputed_weight[b]
|
| 171 |
+
loss_edit[b] += - w * log_u_req
|
| 172 |
+
|
| 173 |
+
# 3. ----- COMBINE -----
|
| 174 |
+
loss = loss_rate + loss_edit # (B,)
|
| 175 |
+
loss = loss.mean()
|
| 176 |
+
return loss
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class EditFlowsLossReParam(_Loss):
|
| 180 |
+
"""
|
| 181 |
+
Edit Flows loss (Eq. 23) but parameterized by:
|
| 182 |
+
- lam_total: (B, L) >= 0
|
| 183 |
+
- pi_type: (B, L, 3) prob over {ins, del, sub}, sums to 1
|
| 184 |
+
|
| 185 |
+
Token distributions are still given by logits_ins/logits_sub.
|
| 186 |
+
|
| 187 |
+
Required-edit log rate:
|
| 188 |
+
del: log u = log lam_total[pos] + log pi_del[pos]
|
| 189 |
+
sub: log u = log lam_total[pos] + log pi_sub[pos] + log Q_sub(token)
|
| 190 |
+
ins: log u = log lam_total[pos] + log pi_ins[pos] + log Q_ins(token)
|
| 191 |
+
"""
|
| 192 |
+
|
| 193 |
+
def __init__(self, reduction: str = "mean", gamma_rate: float = 1.0, eps: float = 1e-12) -> None:
|
| 194 |
+
super().__init__(None, None, reduction)
|
| 195 |
+
self.gamma_rate = float(gamma_rate)
|
| 196 |
+
self.eps = float(eps)
|
| 197 |
+
|
| 198 |
+
def forward(
|
| 199 |
+
self,
|
| 200 |
+
lam_total: torch.Tensor, # (B, L)
|
| 201 |
+
pi_type: torch.Tensor, # (B, L, 3) probabilities over {ins, del, sub}
|
| 202 |
+
logits_ins: torch.Tensor, # (B, L, V)
|
| 203 |
+
logits_sub: torch.Tensor, # (B, L, V)
|
| 204 |
+
z_t: torch.Tensor, # (B, N) aligned, with eps_id
|
| 205 |
+
z_1: torch.Tensor, # (B, N) aligned target, with eps_id
|
| 206 |
+
x_t: torch.Tensor, # (B, L)
|
| 207 |
+
valid_mask: torch.Tensor, # (B, L) bool, False==padding, True==valid (same as your old loss)
|
| 208 |
+
precomputed_weight: torch.Tensor, # (B,) or () = kappa_dot/(1-kappa)
|
| 209 |
+
eps_id: int,
|
| 210 |
+
bos_id: int,
|
| 211 |
+
eos_id: int,
|
| 212 |
+
) -> torch.Tensor:
|
| 213 |
+
device = lam_total.device
|
| 214 |
+
B, L = x_t.shape
|
| 215 |
+
_, N = z_t.shape
|
| 216 |
+
|
| 217 |
+
# --- RATE TERM ---
|
| 218 |
+
valid_f = valid_mask.to(lam_total.dtype) # True=valid -> 1.0
|
| 219 |
+
loss_rate = (lam_total * valid_f).sum(dim=1) # (B,)
|
| 220 |
+
loss_rate = self.gamma_rate * loss_rate
|
| 221 |
+
|
| 222 |
+
# --- EDIT TERM ---
|
| 223 |
+
logp_ins = F.log_softmax(logits_ins, dim=-1) # (B, L, V)
|
| 224 |
+
logp_sub = F.log_softmax(logits_sub, dim=-1) # (B, L, V)
|
| 225 |
+
|
| 226 |
+
# normalize pi_type defensively (in case caller passes slightly off-simplex)
|
| 227 |
+
pi_sum = pi_type.sum(dim=-1, keepdim=True).clamp_min(self.eps)
|
| 228 |
+
pi_type = pi_type / pi_sum
|
| 229 |
+
|
| 230 |
+
log_lam = torch.log(lam_total.clamp_min(self.eps)) # (B, L)
|
| 231 |
+
log_pi = torch.log(pi_type.clamp_min(self.eps)) # (B, L, 3)
|
| 232 |
+
|
| 233 |
+
# weight shape
|
| 234 |
+
if precomputed_weight.dim() == 0:
|
| 235 |
+
precomputed_weight = precomputed_weight.view(1).expand(B).to(device)
|
| 236 |
+
else:
|
| 237 |
+
precomputed_weight = precomputed_weight.to(device)
|
| 238 |
+
|
| 239 |
+
loss_edit = torch.zeros(B, dtype=torch.float32, device=device)
|
| 240 |
+
|
| 241 |
+
for b in range(B):
|
| 242 |
+
valid_len = int(valid_mask[b].sum().item())
|
| 243 |
+
prefix_non_eps = 0
|
| 244 |
+
|
| 245 |
+
for i in range(N):
|
| 246 |
+
zt = int(z_t[b, i].item())
|
| 247 |
+
z1 = int(z_1[b, i].item())
|
| 248 |
+
|
| 249 |
+
# map this alignment column to an x_t position
|
| 250 |
+
if zt != eps_id:
|
| 251 |
+
x_pos = prefix_non_eps
|
| 252 |
+
is_token = True
|
| 253 |
+
prefix_non_eps += 1
|
| 254 |
+
else:
|
| 255 |
+
x_pos = prefix_non_eps
|
| 256 |
+
is_token = False
|
| 257 |
+
|
| 258 |
+
# already matched -> no supervision
|
| 259 |
+
if zt == z1:
|
| 260 |
+
continue
|
| 261 |
+
|
| 262 |
+
# don't force edits where target is BOS/EOS
|
| 263 |
+
if z1 == bos_id or z1 == eos_id:
|
| 264 |
+
continue
|
| 265 |
+
|
| 266 |
+
# CASE 1: deletion (token -> eps)
|
| 267 |
+
if is_token and (z1 == eps_id):
|
| 268 |
+
if x_pos >= valid_len:
|
| 269 |
+
raise NotImplementedError
|
| 270 |
+
|
| 271 |
+
x_token = int(x_t[b, x_pos].item())
|
| 272 |
+
if x_token == bos_id or x_token == eos_id:
|
| 273 |
+
continue
|
| 274 |
+
|
| 275 |
+
log_u_req = log_lam[b, x_pos] + log_pi[b, x_pos, 1] # del index = 1
|
| 276 |
+
|
| 277 |
+
# CASE 2: substitution (token -> different token)
|
| 278 |
+
elif is_token and (z1 != eps_id) and (zt != z1):
|
| 279 |
+
if x_pos >= valid_len:
|
| 280 |
+
raise NotImplementedError
|
| 281 |
+
|
| 282 |
+
x_token = int(x_t[b, x_pos].item())
|
| 283 |
+
if x_token == bos_id or x_token == eos_id:
|
| 284 |
+
continue
|
| 285 |
+
|
| 286 |
+
log_u_req = (
|
| 287 |
+
log_lam[b, x_pos]
|
| 288 |
+
+ log_pi[b, x_pos, 2] # sub index = 2
|
| 289 |
+
+ logp_sub[b, x_pos, z1]
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
# CASE 3: insertion (eps -> token), map gap to "after token (x_pos-1)"
|
| 293 |
+
elif (not is_token) and (z1 != eps_id):
|
| 294 |
+
ins_pos = x_pos - 1
|
| 295 |
+
if ins_pos < 0:
|
| 296 |
+
ins_pos = 0
|
| 297 |
+
|
| 298 |
+
if valid_len == 0:
|
| 299 |
+
ins_pos = 0
|
| 300 |
+
elif ins_pos >= valid_len:
|
| 301 |
+
ins_pos = valid_len - 1
|
| 302 |
+
|
| 303 |
+
x_token = int(x_t[b, ins_pos].item())
|
| 304 |
+
if x_token == eos_id:
|
| 305 |
+
continue
|
| 306 |
+
|
| 307 |
+
log_u_req = (
|
| 308 |
+
log_lam[b, ins_pos]
|
| 309 |
+
+ log_pi[b, ins_pos, 0] # ins index = 0
|
| 310 |
+
+ logp_ins[b, ins_pos, z1]
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
else:
|
| 314 |
+
raise NotImplementedError
|
| 315 |
+
|
| 316 |
+
w = precomputed_weight[b]
|
| 317 |
+
loss_edit[b] += -w * log_u_req
|
| 318 |
+
|
| 319 |
+
loss = loss_rate + loss_edit
|
| 320 |
+
return loss.mean()
|
flow_matching/loss/generalized_loss.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
from torch.nn.modules.loss import _Loss
|
| 10 |
+
|
| 11 |
+
from flow_matching.path import MixtureDiscreteProbPath
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class MixturePathGeneralizedKL(_Loss):
|
| 15 |
+
r"""A generalized KL loss for discrete flow matching.
|
| 16 |
+
A class that measures the generalized KL of a discrete flow model :math:`p_{1|t}` w.r.t. a probability path given by ``path``. Note: this class is assuming that the model is trained on the same path.
|
| 17 |
+
|
| 18 |
+
For a model trained on a space :math:`\mathcal{S} = \mathcal{T}^d`, :math:`\mathcal{T} = [K] = \set{1,2,\ldots,K}`, the loss is given by
|
| 19 |
+
|
| 20 |
+
.. math::
|
| 21 |
+
\ell_i(x_1, x_t, t) = -\frac{\dot{\kappa}_t}{1-\kappa_t} \biggr[ p_{1|t}(x_t^i|x_t) -\delta_{x^i_1}(x_t^i) + (1-\delta_{x^i_1}(x_t^i))\left(\log p_{1|t}(x_1^i|x_t)\right)\biggr],
|
| 22 |
+
|
| 23 |
+
where :math:`\kappa_t` is the scheduler associated with ``path``.
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
path (MixtureDiscreteProbPath): Probability path (x-prediction training).
|
| 27 |
+
reduction (str, optional): Specify the reduction to apply to the output ``'none'`` | ``'mean'`` | ``'sum'``. ``'none'``: no reduction is applied to the output, ``'mean'``: the output is reduced by mean over sequence elements, ``'sum'``: the output is reduced by sum over sequence elements. Defaults to 'mean'.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def __init__(self, path: MixtureDiscreteProbPath, reduction: str = "mean") -> None:
|
| 31 |
+
super().__init__(None, None, reduction)
|
| 32 |
+
self.path = path
|
| 33 |
+
|
| 34 |
+
def forward(self, logits: Tensor, x_1: Tensor, x_t: Tensor, t: Tensor) -> Tensor:
|
| 35 |
+
r"""Evaluates the generalized KL loss.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
logits (Tensor): posterior model output (i.e., softmax(``logits``) :math:`=p_{1|t}(x|x_t)`), shape (batch, d, K).
|
| 39 |
+
x_1 (Tensor): target data point :math:`x_1 \sim q`, shape (batch, d).
|
| 40 |
+
x_t (Tensor): conditional sample at :math:`x_t \sim p_t(\cdot|x_1)`, shape (batch, d).
|
| 41 |
+
t (Tensor): times in :math:`[0,1]`, shape (batch).
|
| 42 |
+
|
| 43 |
+
Raises:
|
| 44 |
+
ValueError: reduction value must be one of ``'none'`` | ``'mean'`` | ``'sum'``.
|
| 45 |
+
|
| 46 |
+
Returns:
|
| 47 |
+
Tensor: Generalized KL loss.
|
| 48 |
+
"""
|
| 49 |
+
x_1_shape = x_1.shape
|
| 50 |
+
|
| 51 |
+
# extract x_1 value of log(p_{1|t}(x|x_t)).
|
| 52 |
+
log_p_1t = torch.log_softmax(logits, dim=-1)
|
| 53 |
+
log_p_1t_x1 = torch.gather(log_p_1t, dim=-1, index=x_1.unsqueeze(-1))
|
| 54 |
+
log_p_1t_x1 = log_p_1t_x1.view(*x_1_shape)
|
| 55 |
+
|
| 56 |
+
# extract x_t value of p_{1|t}(x|x_t).
|
| 57 |
+
p_1t = torch.exp(log_p_1t)
|
| 58 |
+
p_1t_xt = torch.gather(p_1t, dim=-1, index=x_t.unsqueeze(-1))
|
| 59 |
+
p_1t_xt = p_1t_xt.view(*x_1_shape)
|
| 60 |
+
|
| 61 |
+
scheduler_output = self.path.scheduler(t)
|
| 62 |
+
|
| 63 |
+
jump_coefficient = (
|
| 64 |
+
scheduler_output.d_alpha_t / (1 - scheduler_output.alpha_t)
|
| 65 |
+
)[(...,) + (None,) * (x_1.dim() - 1)]
|
| 66 |
+
jump_coefficient = jump_coefficient.repeat(1, *x_1_shape[1:])
|
| 67 |
+
delta_x1_xt = (x_t == x_1).to(log_p_1t.dtype)
|
| 68 |
+
|
| 69 |
+
loss = -jump_coefficient * (
|
| 70 |
+
p_1t_xt - delta_x1_xt + (1 - delta_x1_xt) * log_p_1t_x1
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
if self.reduction == "mean":
|
| 74 |
+
return torch.mean(loss)
|
| 75 |
+
elif self.reduction == "sum":
|
| 76 |
+
return torch.sum(loss)
|
| 77 |
+
elif self.reduction == "none":
|
| 78 |
+
return loss
|
| 79 |
+
else:
|
| 80 |
+
raise ValueError(f"{self.reduction} is not a valid value for reduction")
|
flow_matching/path/.DS_Store
ADDED
|
Binary file (8.2 kB). View file
|
|
|
flow_matching/path/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
from .affine import AffineProbPath, CondOTProbPath
|
| 8 |
+
from .geodesic import GeodesicProbPath
|
| 9 |
+
from .mixture import MixtureDiscreteProbPath
|
| 10 |
+
from .path import ProbPath
|
| 11 |
+
from .path_sample import DiscretePathSample, PathSample
|
| 12 |
+
from .editflows_adapter import EditFlowsPathAdapter
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
__all__ = [
|
| 16 |
+
"ProbPath",
|
| 17 |
+
"AffineProbPath",
|
| 18 |
+
"CondOTProbPath",
|
| 19 |
+
"MixtureDiscreteProbPath",
|
| 20 |
+
"GeodesicProbPath",
|
| 21 |
+
"PathSample",
|
| 22 |
+
"DiscretePathSample",
|
| 23 |
+
"EditFlowsPathAdapter",
|
| 24 |
+
]
|
flow_matching/path/affine.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
from torch import Tensor
|
| 8 |
+
|
| 9 |
+
from flow_matching.path.path import ProbPath
|
| 10 |
+
from flow_matching.path.path_sample import PathSample
|
| 11 |
+
from flow_matching.path.scheduler.scheduler import CondOTScheduler, Scheduler
|
| 12 |
+
from flow_matching.utils import expand_tensor_like
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class AffineProbPath(ProbPath):
|
| 16 |
+
r"""The ``AffineProbPath`` class represents a specific type of probability path where the transformation between distributions is affine.
|
| 17 |
+
An affine transformation can be represented as:
|
| 18 |
+
|
| 19 |
+
.. math::
|
| 20 |
+
|
| 21 |
+
X_t = \alpha_t X_1 + \sigma_t X_0,
|
| 22 |
+
|
| 23 |
+
where :math:`X_t` is the transformed data point at time `t`. :math:`X_0` and :math:`X_1` are the source and target data points, respectively. :math:`\alpha_t` and :math:`\sigma_t` are the parameters of the affine transformation at time `t`.
|
| 24 |
+
|
| 25 |
+
The scheduler is responsible for providing the time-dependent parameters :math:`\alpha_t` and :math:`\sigma_t`, as well as their derivatives, which define the affine transformation at any given time `t`.
|
| 26 |
+
|
| 27 |
+
Using ``AffineProbPath`` in the flow matching framework:
|
| 28 |
+
|
| 29 |
+
.. code-block:: python
|
| 30 |
+
|
| 31 |
+
# Instantiates a probability path
|
| 32 |
+
my_path = AffineProbPath(...)
|
| 33 |
+
mse_loss = torch.nn.MSELoss()
|
| 34 |
+
|
| 35 |
+
for x_1 in dataset:
|
| 36 |
+
# Sets x_0 to random noise
|
| 37 |
+
x_0 = torch.randn()
|
| 38 |
+
|
| 39 |
+
# Sets t to a random value in [0,1]
|
| 40 |
+
t = torch.rand()
|
| 41 |
+
|
| 42 |
+
# Samples the conditional path X_t ~ p_t(X_t|X_0,X_1)
|
| 43 |
+
path_sample = my_path.sample(x_0=x_0, x_1=x_1, t=t)
|
| 44 |
+
|
| 45 |
+
# Computes the MSE loss w.r.t. the velocity
|
| 46 |
+
loss = mse_loss(path_sample.dx_t, my_model(x_t, t))
|
| 47 |
+
loss.backward()
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
scheduler (Scheduler): An instance of a scheduler that provides the parameters :math:`\alpha_t`, :math:`\sigma_t`, and their derivatives over time.
|
| 51 |
+
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
def __init__(self, scheduler: Scheduler):
|
| 55 |
+
self.scheduler = scheduler
|
| 56 |
+
|
| 57 |
+
def sample(self, x_0: Tensor, x_1: Tensor, t: Tensor) -> PathSample:
|
| 58 |
+
r"""Sample from the affine probability path:
|
| 59 |
+
|
| 60 |
+
| given :math:`(X_0,X_1) \sim \pi(X_0,X_1)` and a scheduler :math:`(\alpha_t,\sigma_t)`.
|
| 61 |
+
| return :math:`X_0, X_1, X_t = \alpha_t X_1 + \sigma_t X_0`, and the conditional velocity at :math:`X_t, \dot{X}_t = \dot{\alpha}_t X_1 + \dot{\sigma}_t X_0`.
|
| 62 |
+
|
| 63 |
+
Args:
|
| 64 |
+
x_0 (Tensor): source data point, shape (batch_size, ...).
|
| 65 |
+
x_1 (Tensor): target data point, shape (batch_size, ...).
|
| 66 |
+
t (Tensor): times in [0,1], shape (batch_size).
|
| 67 |
+
|
| 68 |
+
Returns:
|
| 69 |
+
PathSample: a conditional sample at :math:`X_t \sim p_t`.
|
| 70 |
+
"""
|
| 71 |
+
self.assert_sample_shape(x_0=x_0, x_1=x_1, t=t)
|
| 72 |
+
|
| 73 |
+
scheduler_output = self.scheduler(t)
|
| 74 |
+
|
| 75 |
+
alpha_t = expand_tensor_like(
|
| 76 |
+
input_tensor=scheduler_output.alpha_t, expand_to=x_1
|
| 77 |
+
)
|
| 78 |
+
sigma_t = expand_tensor_like(
|
| 79 |
+
input_tensor=scheduler_output.sigma_t, expand_to=x_1
|
| 80 |
+
)
|
| 81 |
+
d_alpha_t = expand_tensor_like(
|
| 82 |
+
input_tensor=scheduler_output.d_alpha_t, expand_to=x_1
|
| 83 |
+
)
|
| 84 |
+
d_sigma_t = expand_tensor_like(
|
| 85 |
+
input_tensor=scheduler_output.d_sigma_t, expand_to=x_1
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
# construct xt ~ p_t(x|x1).
|
| 89 |
+
x_t = sigma_t * x_0 + alpha_t * x_1
|
| 90 |
+
dx_t = d_sigma_t * x_0 + d_alpha_t * x_1
|
| 91 |
+
|
| 92 |
+
return PathSample(x_t=x_t, dx_t=dx_t, x_1=x_1, x_0=x_0, t=t)
|
| 93 |
+
|
| 94 |
+
def target_to_velocity(self, x_1: Tensor, x_t: Tensor, t: Tensor) -> Tensor:
|
| 95 |
+
r"""Convert from x_1 representation to velocity.
|
| 96 |
+
|
| 97 |
+
| given :math:`X_1`.
|
| 98 |
+
| return :math:`\dot{X}_t`.
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
x_1 (Tensor): target data point.
|
| 102 |
+
x_t (Tensor): path sample at time t.
|
| 103 |
+
t (Tensor): time in [0,1].
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
Tensor: velocity.
|
| 107 |
+
"""
|
| 108 |
+
scheduler_output = self.scheduler(t)
|
| 109 |
+
|
| 110 |
+
alpha_t = scheduler_output.alpha_t
|
| 111 |
+
d_alpha_t = scheduler_output.d_alpha_t
|
| 112 |
+
sigma_t = scheduler_output.sigma_t
|
| 113 |
+
d_sigma_t = scheduler_output.d_sigma_t
|
| 114 |
+
|
| 115 |
+
a_t = d_sigma_t / sigma_t
|
| 116 |
+
b_t = (d_alpha_t * sigma_t - d_sigma_t * alpha_t) / sigma_t
|
| 117 |
+
|
| 118 |
+
return a_t * x_t + b_t * x_1
|
| 119 |
+
|
| 120 |
+
def epsilon_to_velocity(self, epsilon: Tensor, x_t: Tensor, t: Tensor) -> Tensor:
|
| 121 |
+
r"""Convert from epsilon representation to velocity.
|
| 122 |
+
|
| 123 |
+
| given :math:`\epsilon`.
|
| 124 |
+
| return :math:`\dot{X}_t`.
|
| 125 |
+
|
| 126 |
+
Args:
|
| 127 |
+
epsilon (Tensor): noise in the path sample.
|
| 128 |
+
x_t (Tensor): path sample at time t.
|
| 129 |
+
t (Tensor): time in [0,1].
|
| 130 |
+
|
| 131 |
+
Returns:
|
| 132 |
+
Tensor: velocity.
|
| 133 |
+
"""
|
| 134 |
+
scheduler_output = self.scheduler(t)
|
| 135 |
+
|
| 136 |
+
alpha_t = scheduler_output.alpha_t
|
| 137 |
+
d_alpha_t = scheduler_output.d_alpha_t
|
| 138 |
+
sigma_t = scheduler_output.sigma_t
|
| 139 |
+
d_sigma_t = scheduler_output.d_sigma_t
|
| 140 |
+
|
| 141 |
+
a_t = d_alpha_t / alpha_t
|
| 142 |
+
b_t = (d_sigma_t * alpha_t - d_alpha_t * sigma_t) / alpha_t
|
| 143 |
+
|
| 144 |
+
return a_t * x_t + b_t * epsilon
|
| 145 |
+
|
| 146 |
+
def velocity_to_target(self, velocity: Tensor, x_t: Tensor, t: Tensor) -> Tensor:
|
| 147 |
+
r"""Convert from velocity to x_1 representation.
|
| 148 |
+
|
| 149 |
+
| given :math:`\dot{X}_t`.
|
| 150 |
+
| return :math:`X_1`.
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
velocity (Tensor): velocity at the path sample.
|
| 154 |
+
x_t (Tensor): path sample at time t.
|
| 155 |
+
t (Tensor): time in [0,1].
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
Tensor: target data point.
|
| 159 |
+
"""
|
| 160 |
+
scheduler_output = self.scheduler(t)
|
| 161 |
+
|
| 162 |
+
alpha_t = scheduler_output.alpha_t
|
| 163 |
+
d_alpha_t = scheduler_output.d_alpha_t
|
| 164 |
+
sigma_t = scheduler_output.sigma_t
|
| 165 |
+
d_sigma_t = scheduler_output.d_sigma_t
|
| 166 |
+
|
| 167 |
+
a_t = -d_sigma_t / (d_alpha_t * sigma_t - d_sigma_t * alpha_t)
|
| 168 |
+
b_t = sigma_t / (d_alpha_t * sigma_t - d_sigma_t * alpha_t)
|
| 169 |
+
|
| 170 |
+
return a_t * x_t + b_t * velocity
|
| 171 |
+
|
| 172 |
+
def epsilon_to_target(self, epsilon: Tensor, x_t: Tensor, t: Tensor) -> Tensor:
|
| 173 |
+
r"""Convert from epsilon representation to x_1 representation.
|
| 174 |
+
|
| 175 |
+
| given :math:`\epsilon`.
|
| 176 |
+
| return :math:`X_1`.
|
| 177 |
+
|
| 178 |
+
Args:
|
| 179 |
+
epsilon (Tensor): noise in the path sample.
|
| 180 |
+
x_t (Tensor): path sample at time t.
|
| 181 |
+
t (Tensor): time in [0,1].
|
| 182 |
+
|
| 183 |
+
Returns:
|
| 184 |
+
Tensor: target data point.
|
| 185 |
+
"""
|
| 186 |
+
scheduler_output = self.scheduler(t)
|
| 187 |
+
|
| 188 |
+
alpha_t = scheduler_output.alpha_t
|
| 189 |
+
sigma_t = scheduler_output.sigma_t
|
| 190 |
+
|
| 191 |
+
a_t = 1 / alpha_t
|
| 192 |
+
b_t = -sigma_t / alpha_t
|
| 193 |
+
|
| 194 |
+
return a_t * x_t + b_t * epsilon
|
| 195 |
+
|
| 196 |
+
def velocity_to_epsilon(self, velocity: Tensor, x_t: Tensor, t: Tensor) -> Tensor:
|
| 197 |
+
r"""Convert from velocity to noise representation.
|
| 198 |
+
|
| 199 |
+
| given :math:`\dot{X}_t`.
|
| 200 |
+
| return :math:`\epsilon`.
|
| 201 |
+
|
| 202 |
+
Args:
|
| 203 |
+
velocity (Tensor): velocity at the path sample.
|
| 204 |
+
x_t (Tensor): path sample at time t.
|
| 205 |
+
t (Tensor): time in [0,1].
|
| 206 |
+
|
| 207 |
+
Returns:
|
| 208 |
+
Tensor: noise in the path sample.
|
| 209 |
+
"""
|
| 210 |
+
scheduler_output = self.scheduler(t)
|
| 211 |
+
|
| 212 |
+
alpha_t = scheduler_output.alpha_t
|
| 213 |
+
d_alpha_t = scheduler_output.d_alpha_t
|
| 214 |
+
sigma_t = scheduler_output.sigma_t
|
| 215 |
+
d_sigma_t = scheduler_output.d_sigma_t
|
| 216 |
+
|
| 217 |
+
a_t = -d_alpha_t / (d_sigma_t * alpha_t - d_alpha_t * sigma_t)
|
| 218 |
+
b_t = alpha_t / (d_sigma_t * alpha_t - d_alpha_t * sigma_t)
|
| 219 |
+
|
| 220 |
+
return a_t * x_t + b_t * velocity
|
| 221 |
+
|
| 222 |
+
def target_to_epsilon(self, x_1: Tensor, x_t: Tensor, t: Tensor) -> Tensor:
|
| 223 |
+
r"""Convert from x_1 representation to velocity.
|
| 224 |
+
|
| 225 |
+
| given :math:`X_1`.
|
| 226 |
+
| return :math:`\epsilon`.
|
| 227 |
+
|
| 228 |
+
Args:
|
| 229 |
+
x_1 (Tensor): target data point.
|
| 230 |
+
x_t (Tensor): path sample at time t.
|
| 231 |
+
t (Tensor): time in [0,1].
|
| 232 |
+
|
| 233 |
+
Returns:
|
| 234 |
+
Tensor: noise in the path sample.
|
| 235 |
+
"""
|
| 236 |
+
scheduler_output = self.scheduler(t)
|
| 237 |
+
|
| 238 |
+
alpha_t = scheduler_output.alpha_t
|
| 239 |
+
sigma_t = scheduler_output.sigma_t
|
| 240 |
+
|
| 241 |
+
a_t = 1 / sigma_t
|
| 242 |
+
b_t = -alpha_t / sigma_t
|
| 243 |
+
|
| 244 |
+
return a_t * x_t + b_t * x_1
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
class CondOTProbPath(AffineProbPath):
|
| 248 |
+
r"""The ``CondOTProbPath`` class represents a conditional optimal transport probability path.
|
| 249 |
+
|
| 250 |
+
This class is a specialized version of the ``AffineProbPath`` that uses a conditional optimal transport scheduler to determine the parameters of the affine transformation.
|
| 251 |
+
|
| 252 |
+
The parameters :math:`\alpha_t` and :math:`\sigma_t` for the conditional optimal transport path are defined as:
|
| 253 |
+
|
| 254 |
+
.. math::
|
| 255 |
+
|
| 256 |
+
\alpha_t = t \quad \text{and} \quad \sigma_t = 1 - t.
|
| 257 |
+
"""
|
| 258 |
+
|
| 259 |
+
def __init__(self):
|
| 260 |
+
self.scheduler = CondOTScheduler()
|
flow_matching/path/editflows_adapter.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import List, Sequence, Optional, Tuple
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
from torch import Tensor
|
| 6 |
+
import pdb
|
| 7 |
+
|
| 8 |
+
from flow_matching.path import MixtureDiscreteProbPath
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass
|
| 12 |
+
class EditFlowsPathSampleRagged:
|
| 13 |
+
# aligned inputs/outputs (ragged)
|
| 14 |
+
z0_list: List[Tensor] # list[(T_i,)]
|
| 15 |
+
z1_list: List[Tensor] # list[(T_i,)]
|
| 16 |
+
zt_list: List[Tensor] # list[(T_i,)]
|
| 17 |
+
# projected current sequences (ragged X_t)
|
| 18 |
+
x_t_list: List[Tensor] # list[(n_i,)]
|
| 19 |
+
# targets/masks for EF loss (ragged)
|
| 20 |
+
need_delete_list: List[Tensor] # list[(n_i,)] bool
|
| 21 |
+
need_substitute_list: List[Tensor] # list[(n_i,)] bool
|
| 22 |
+
sub_target_list: List[Tensor] # list[(n_i,)] long
|
| 23 |
+
ins_slot_idx_list: List[Tensor] # list[(K_i,)] long indices in [0..n_i]
|
| 24 |
+
ins_target_list: List[Tensor] # list[(K_i,)] long
|
| 25 |
+
# time
|
| 26 |
+
t: Tensor # (B,)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class EditFlowsPathAdapter:
|
| 30 |
+
"""
|
| 31 |
+
Ragged adapter for Edit Flows:
|
| 32 |
+
- Accepts aligned Z sequences as tensors (B, N) (may include eps_id).
|
| 33 |
+
- Applies MixtureDiscreteProbPath's scheduler (σ_t) to sample z_t by
|
| 34 |
+
flipping z0→z1 with prob (1-σ_t) per aligned token.
|
| 35 |
+
- You can later project z_t -> x_t by removing ε.
|
| 36 |
+
"""
|
| 37 |
+
def __init__(self, mixture_path, eps_id: int):
|
| 38 |
+
self.path = mixture_path
|
| 39 |
+
self.eps_id = int(eps_id)
|
| 40 |
+
|
| 41 |
+
# expose scheduler so training can precompute weights κ̇/(1-κ)
|
| 42 |
+
def scheduler(self, t: Tensor):
|
| 43 |
+
return self.path.scheduler(t)
|
| 44 |
+
|
| 45 |
+
@torch.no_grad()
|
| 46 |
+
def sample(
|
| 47 |
+
self,
|
| 48 |
+
z0: Tensor, # (B, N)
|
| 49 |
+
z1: Tensor, # (B, N)
|
| 50 |
+
t: Tensor, # () or (B,)
|
| 51 |
+
) -> Tensor:
|
| 52 |
+
"""
|
| 53 |
+
Sample z_t from (z0, z1) using the discrete mixture path:
|
| 54 |
+
- σ_t = scheduler(t)
|
| 55 |
+
- with prob σ_t keep z0
|
| 56 |
+
- with prob (1 - σ_t) take z1
|
| 57 |
+
We do this per-column.
|
| 58 |
+
BOS/EOS are already aligned -> z0 == z1 there -> safe.
|
| 59 |
+
"""
|
| 60 |
+
device = z0.device
|
| 61 |
+
B, N = z0.shape
|
| 62 |
+
|
| 63 |
+
# get sigma_t from the path
|
| 64 |
+
sigma = self.path.scheduler(t).sigma_t # could be scalar or (B,)
|
| 65 |
+
if sigma.dim() == 0:
|
| 66 |
+
sigma = sigma.expand(B) # (B,)
|
| 67 |
+
else:
|
| 68 |
+
# ensure shape is (B,)
|
| 69 |
+
sigma = sigma.view(B)
|
| 70 |
+
# reshape to broadcast over columns
|
| 71 |
+
sigma = sigma.view(B, 1) # (B, 1)
|
| 72 |
+
|
| 73 |
+
# uniform noise per position
|
| 74 |
+
u = torch.rand(B, N, device=device)
|
| 75 |
+
|
| 76 |
+
# we only need to flip where z0 != z1
|
| 77 |
+
diff_mask = (z0 != z1) # (B, N)
|
| 78 |
+
|
| 79 |
+
# flip where u > sigma (i.e. prob 1 - sigma) AND tokens differ
|
| 80 |
+
flip = (u > sigma) & diff_mask # (B, N)
|
| 81 |
+
|
| 82 |
+
# choose from z1 when flip, else z0
|
| 83 |
+
z_t = torch.where(flip, z1, z0) # (B, N)
|
| 84 |
+
|
| 85 |
+
return z_t
|
flow_matching/path/geodesic.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
from torch import Tensor
|
| 10 |
+
from torch.func import jvp, vmap
|
| 11 |
+
|
| 12 |
+
from flow_matching.path.path import ProbPath
|
| 13 |
+
|
| 14 |
+
from flow_matching.path.path_sample import PathSample
|
| 15 |
+
from flow_matching.path.scheduler import ConvexScheduler
|
| 16 |
+
from flow_matching.utils import expand_tensor_like
|
| 17 |
+
|
| 18 |
+
from flow_matching.utils.manifolds import geodesic, Manifold
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class GeodesicProbPath(ProbPath):
|
| 22 |
+
r"""The ``GeodesicProbPath`` class represents a specific type of probability path where the transformation between distributions is defined through the geodesic path.
|
| 23 |
+
Mathematically, a geodesic path can be represented as:
|
| 24 |
+
|
| 25 |
+
.. math::
|
| 26 |
+
|
| 27 |
+
X_t = \psi_t(X_0 | X_1) = \exp_{X_1}(\kappa_t \log_{X_1}(X_0)),
|
| 28 |
+
|
| 29 |
+
where :math:`X_t` is the transformed data point at time `t`, :math:`X_0` and :math:`X_1` are the source and target data points, respectively, and :math:`\kappa_t` is a scheduler.
|
| 30 |
+
|
| 31 |
+
The scheduler is responsible for providing the time-dependent :math:`\kappa_t` and must be differentiable.
|
| 32 |
+
|
| 33 |
+
Using ``GeodesicProbPath`` in the flow matching framework:
|
| 34 |
+
|
| 35 |
+
.. code-block:: python
|
| 36 |
+
# Instantiates a manifold
|
| 37 |
+
manifold = FlatTorus()
|
| 38 |
+
|
| 39 |
+
# Instantiates a scheduler
|
| 40 |
+
scheduler = CondOTScheduler()
|
| 41 |
+
|
| 42 |
+
# Instantiates a probability path
|
| 43 |
+
my_path = GeodesicProbPath(scheduler, manifold)
|
| 44 |
+
mse_loss = torch.nn.MSELoss()
|
| 45 |
+
|
| 46 |
+
for x_1 in dataset:
|
| 47 |
+
# Sets x_0 to random noise
|
| 48 |
+
x_0 = torch.randn()
|
| 49 |
+
|
| 50 |
+
# Sets t to a random value in [0,1]
|
| 51 |
+
t = torch.rand()
|
| 52 |
+
|
| 53 |
+
# Samples the conditional path :math:`X_t \sim p_t(X_t|X_0,X_1)`
|
| 54 |
+
path_sample = my_path.sample(x_0=x_0, x_1=x_1, t=t)
|
| 55 |
+
|
| 56 |
+
# Computes the MSE loss w.r.t. the velocity
|
| 57 |
+
loss = mse_loss(path_sample.dx_t, my_model(x_t, t))
|
| 58 |
+
loss.backward()
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
scheduler (ConvexScheduler): The scheduler that provides :math:`\kappa_t`.
|
| 62 |
+
manifold (Manifold): The manifold on which the probability path is defined.
|
| 63 |
+
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
def __init__(self, scheduler: ConvexScheduler, manifold: Manifold):
|
| 67 |
+
self.scheduler = scheduler
|
| 68 |
+
self.manifold = manifold
|
| 69 |
+
|
| 70 |
+
def sample(self, x_0: Tensor, x_1: Tensor, t: Tensor) -> PathSample:
|
| 71 |
+
r"""Sample from the Riemannian probability path with geodesic interpolation:
|
| 72 |
+
|
| 73 |
+
| given :math:`(X_0,X_1) \sim \pi(X_0,X_1)` and a scheduler :math:`\kappa_t`.
|
| 74 |
+
| return :math:`X_0, X_1, X_t = \exp_{X_1}(\kappa_t \log_{X_1}(X_0))`, and the conditional velocity at :math:`X_t, \dot{X}_t`.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
x_0 (Tensor): source data point, shape (batch_size, ...).
|
| 78 |
+
x_1 (Tensor): target data point, shape (batch_size, ...).
|
| 79 |
+
t (Tensor): times in [0,1], shape (batch_size).
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
PathSample: A conditional sample at :math:`X_t \sim p_t`.
|
| 83 |
+
"""
|
| 84 |
+
self.assert_sample_shape(x_0=x_0, x_1=x_1, t=t)
|
| 85 |
+
t = expand_tensor_like(input_tensor=t, expand_to=x_1[..., 0:1]).clone()
|
| 86 |
+
|
| 87 |
+
def cond_u(x_0, x_1, t):
|
| 88 |
+
path = geodesic(self.manifold, x_0, x_1)
|
| 89 |
+
x_t, dx_t = jvp(
|
| 90 |
+
lambda t: path(self.scheduler(t).alpha_t),
|
| 91 |
+
(t,),
|
| 92 |
+
(torch.ones_like(t).to(t),),
|
| 93 |
+
)
|
| 94 |
+
return x_t, dx_t
|
| 95 |
+
|
| 96 |
+
x_t, dx_t = vmap(cond_u)(x_0, x_1, t)
|
| 97 |
+
x_t = x_t.reshape_as(x_1)
|
| 98 |
+
dx_t = dx_t.reshape_as(x_1)
|
| 99 |
+
|
| 100 |
+
return PathSample(x_t=x_t, dx_t=dx_t, x_1=x_1, x_0=x_0, t=t)
|
flow_matching/path/mixture.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
|
| 10 |
+
from torch import Tensor
|
| 11 |
+
|
| 12 |
+
from flow_matching.path.path import ProbPath
|
| 13 |
+
|
| 14 |
+
from flow_matching.path.path_sample import DiscretePathSample
|
| 15 |
+
from flow_matching.path.scheduler import ConvexScheduler
|
| 16 |
+
from flow_matching.utils import expand_tensor_like, unsqueeze_to_match
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class MixtureDiscreteProbPath(ProbPath):
|
| 20 |
+
r"""The ``MixtureDiscreteProbPath`` class defines a factorized discrete probability path.
|
| 21 |
+
|
| 22 |
+
This path remains constant at the source data point :math:`X_0` until a random time, determined by the scheduler, when it flips to the target data point :math:`X_1`.
|
| 23 |
+
The scheduler determines the flip probability using the parameter :math:`\sigma_t`, which is a function of time `t`. Specifically, :math:`\sigma_t` represents the probability of remaining at :math:`X_0`, while :math:`1 - \sigma_t` is the probability of flipping to :math:`X_1`:
|
| 24 |
+
|
| 25 |
+
.. math::
|
| 26 |
+
|
| 27 |
+
P(X_t = X_0) = \sigma_t \quad \text{and} \quad P(X_t = X_1) = 1 - \sigma_t,
|
| 28 |
+
|
| 29 |
+
where :math:`\sigma_t` is provided by the scheduler.
|
| 30 |
+
|
| 31 |
+
Example:
|
| 32 |
+
|
| 33 |
+
.. code-block:: python
|
| 34 |
+
|
| 35 |
+
>>> x_0 = torch.zeros((1, 3, 3))
|
| 36 |
+
>>> x_1 = torch.ones((1, 3, 3))
|
| 37 |
+
|
| 38 |
+
>>> path = MixtureDiscreteProbPath(PolynomialConvexScheduler(n=1.0))
|
| 39 |
+
>>> result = path.sample(x_0, x_1, t=torch.tensor([0.1])).x_t
|
| 40 |
+
>>> result
|
| 41 |
+
tensor([[[0.0, 0.0, 0.0],
|
| 42 |
+
[0.0, 0.0, 1.0],
|
| 43 |
+
[0.0, 0.0, 0.0]]])
|
| 44 |
+
|
| 45 |
+
>>> result = path.sample(x_0, x_1, t=torch.tensor([0.5])).x_t
|
| 46 |
+
>>> result
|
| 47 |
+
tensor([[[1.0, 0.0, 1.0],
|
| 48 |
+
[0.0, 1.0, 0.0],
|
| 49 |
+
[0.0, 1.0, 0.0]]])
|
| 50 |
+
|
| 51 |
+
>>> result = path.sample(x_0, x_1, t=torch.tensor([1.0])).x_t
|
| 52 |
+
>>> result
|
| 53 |
+
tensor([[[1.0, 1.0, 1.0],
|
| 54 |
+
[1.0, 1.0, 1.0],
|
| 55 |
+
[1.0, 1.0, 1.0]]])
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
scheduler (ConvexScheduler): The scheduler that provides :math:`\sigma_t`.
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
def __init__(self, scheduler: ConvexScheduler):
|
| 62 |
+
assert isinstance(
|
| 63 |
+
scheduler, ConvexScheduler
|
| 64 |
+
), "Scheduler for ConvexProbPath must be a ConvexScheduler."
|
| 65 |
+
|
| 66 |
+
self.scheduler = scheduler
|
| 67 |
+
|
| 68 |
+
def sample(self, x_0: Tensor, x_1: Tensor, t: Tensor) -> DiscretePathSample:
|
| 69 |
+
r"""Sample from the affine probability path:
|
| 70 |
+
| given :math:`(X_0,X_1) \sim \pi(X_0,X_1)` and a scheduler :math:`(\alpha_t,\sigma_t)`.
|
| 71 |
+
| return :math:`X_0, X_1, t`, and :math:`X_t \sim p_t`.
|
| 72 |
+
Args:
|
| 73 |
+
x_0 (Tensor): source data point, shape (batch_size, ...).
|
| 74 |
+
x_1 (Tensor): target data point, shape (batch_size, ...).
|
| 75 |
+
t (Tensor): times in [0,1], shape (batch_size).
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
DiscretePathSample: a conditional sample at :math:`X_t ~ p_t`.
|
| 79 |
+
"""
|
| 80 |
+
self.assert_sample_shape(x_0=x_0, x_1=x_1, t=t)
|
| 81 |
+
|
| 82 |
+
sigma_t = self.scheduler(t).sigma_t
|
| 83 |
+
|
| 84 |
+
sigma_t = expand_tensor_like(input_tensor=sigma_t, expand_to=x_1)
|
| 85 |
+
|
| 86 |
+
source_indices = torch.rand(size=x_1.shape, device=x_1.device) < sigma_t
|
| 87 |
+
x_t = torch.where(condition=source_indices, input=x_0, other=x_1)
|
| 88 |
+
|
| 89 |
+
return DiscretePathSample(x_t=x_t, x_1=x_1, x_0=x_0, t=t)
|
| 90 |
+
|
| 91 |
+
def posterior_to_velocity(
|
| 92 |
+
self, posterior_logits: Tensor, x_t: Tensor, t: Tensor
|
| 93 |
+
) -> Tensor:
|
| 94 |
+
r"""Convert the factorized posterior to velocity.
|
| 95 |
+
|
| 96 |
+
| given :math:`p(X_1|X_t)`. In the factorized case: :math:`\prod_i p(X_1^i | X_t)`.
|
| 97 |
+
| return :math:`u_t`.
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
posterior_logits (Tensor): logits of the x_1 posterior conditional on x_t, shape (..., vocab size).
|
| 101 |
+
x_t (Tensor): path sample at time t, shape (...).
|
| 102 |
+
t (Tensor): time in [0,1].
|
| 103 |
+
|
| 104 |
+
Returns:
|
| 105 |
+
Tensor: velocity.
|
| 106 |
+
"""
|
| 107 |
+
posterior = torch.softmax(posterior_logits, dim=-1)
|
| 108 |
+
vocabulary_size = posterior.shape[-1]
|
| 109 |
+
x_t = F.one_hot(x_t, num_classes=vocabulary_size)
|
| 110 |
+
t = unsqueeze_to_match(source=t, target=x_t)
|
| 111 |
+
|
| 112 |
+
scheduler_output = self.scheduler(t)
|
| 113 |
+
|
| 114 |
+
kappa_t = scheduler_output.alpha_t
|
| 115 |
+
d_kappa_t = scheduler_output.d_alpha_t
|
| 116 |
+
|
| 117 |
+
return (d_kappa_t / (1 - kappa_t)) * (posterior - x_t)
|
flow_matching/path/path.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
from abc import ABC, abstractmethod
|
| 8 |
+
|
| 9 |
+
from torch import Tensor
|
| 10 |
+
|
| 11 |
+
from flow_matching.path.path_sample import PathSample
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class ProbPath(ABC):
|
| 15 |
+
r"""Abstract class, representing a probability path.
|
| 16 |
+
|
| 17 |
+
A probability path transforms the distribution :math:`p(X_0)` into :math:`p(X_1)` over :math:`t=0\rightarrow 1`.
|
| 18 |
+
|
| 19 |
+
The ``ProbPath`` class is designed to support model training in the flow matching framework. It supports two key functionalities: (1) sampling the conditional probability path and (2) conversion between various training objectives.
|
| 20 |
+
Here is a high-level example
|
| 21 |
+
|
| 22 |
+
.. code-block:: python
|
| 23 |
+
|
| 24 |
+
# Instantiate a probability path
|
| 25 |
+
my_path = ProbPath(...)
|
| 26 |
+
|
| 27 |
+
for x_0, x_1 in dataset:
|
| 28 |
+
# Sets t to a random value in [0,1]
|
| 29 |
+
t = torch.rand()
|
| 30 |
+
|
| 31 |
+
# Samples the conditional path X_t ~ p_t(X_t|X_0,X_1)
|
| 32 |
+
path_sample = my_path.sample(x_0=x_0, x_1=x_1, t=t)
|
| 33 |
+
|
| 34 |
+
# Optimizes the model. The loss function varies, depending on model and path.
|
| 35 |
+
loss(path_sample, my_model(x_t, t)).backward()
|
| 36 |
+
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
@abstractmethod
|
| 40 |
+
def sample(self, x_0: Tensor, x_1: Tensor, t: Tensor) -> PathSample:
|
| 41 |
+
r"""Sample from an abstract probability path:
|
| 42 |
+
|
| 43 |
+
| given :math:`(X_0,X_1) \sim \pi(X_0,X_1)`.
|
| 44 |
+
| returns :math:`X_0, X_1, X_t \sim p_t(X_t)`, and a conditional target :math:`Y`, all objects are under ``PathSample``.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
x_0 (Tensor): source data point, shape (batch_size, ...).
|
| 48 |
+
x_1 (Tensor): target data point, shape (batch_size, ...).
|
| 49 |
+
t (Tensor): times in [0,1], shape (batch_size).
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
PathSample: a conditional sample.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
def assert_sample_shape(self, x_0, x_1, t):
|
| 56 |
+
# x_0, x_1: list[Tensor] of variable length (ragged)
|
| 57 |
+
# t: Tensor of shape (B,)
|
| 58 |
+
assert isinstance(x_0, (list, tuple)), f"x_0 must be a list or tuple of ragged Tensors. Got {type(x_0)}"
|
| 59 |
+
assert isinstance(x_1, (list, tuple)), f"x_1 must be a list or tuple of ragged Tensors. Got {type(x_1)}"
|
| 60 |
+
assert isinstance(t, Tensor), f"t must be a torch.Tensor. Got {type(t)}"
|
| 61 |
+
batch_size = len(x_0)
|
| 62 |
+
assert len(x_1) == batch_size, f"x_1 must match x_0 batch size ({batch_size}). Got {len(x_1)}"
|
| 63 |
+
assert t.ndim == 1, f"The time vector t must have shape [batch_size]. Got {t.shape}."
|
| 64 |
+
assert t.shape[0] == batch_size, f"Time t dimension must match the batch size [{batch_size}]. Got {t.shape}"
|
flow_matching/path/path_sample.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
from dataclasses import dataclass, field
|
| 8 |
+
|
| 9 |
+
from torch import Tensor
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class PathSample:
|
| 14 |
+
r"""Represents a sample of a conditional-flow generated probability path.
|
| 15 |
+
|
| 16 |
+
Attributes:
|
| 17 |
+
x_1 (Tensor): the target sample :math:`X_1`.
|
| 18 |
+
x_0 (Tensor): the source sample :math:`X_0`.
|
| 19 |
+
t (Tensor): the time sample :math:`t`.
|
| 20 |
+
x_t (Tensor): samples :math:`X_t \sim p_t(X_t)`, shape (batch_size, ...).
|
| 21 |
+
dx_t (Tensor): conditional target :math:`\frac{\partial X}{\partial t}`, shape: (batch_size, ...).
|
| 22 |
+
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
x_1: Tensor = field(metadata={"help": "target samples X_1 (batch_size, ...)."})
|
| 26 |
+
x_0: Tensor = field(metadata={"help": "source samples X_0 (batch_size, ...)."})
|
| 27 |
+
t: Tensor = field(metadata={"help": "time samples t (batch_size, ...)."})
|
| 28 |
+
x_t: Tensor = field(
|
| 29 |
+
metadata={"help": "samples x_t ~ p_t(X_t), shape (batch_size, ...)."}
|
| 30 |
+
)
|
| 31 |
+
dx_t: Tensor = field(
|
| 32 |
+
metadata={"help": "conditional target dX_t, shape: (batch_size, ...)."}
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@dataclass
|
| 37 |
+
class DiscretePathSample:
|
| 38 |
+
r"""
|
| 39 |
+
Represents a sample of a conditional-flow generated discrete probability path.
|
| 40 |
+
|
| 41 |
+
Attributes:
|
| 42 |
+
x_1 (Tensor): the target sample :math:`X_1`.
|
| 43 |
+
x_0 (Tensor): the source sample :math:`X_0`.
|
| 44 |
+
t (Tensor): the time sample :math:`t`.
|
| 45 |
+
x_t (Tensor): the sample along the path :math:`X_t \sim p_t`.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
x_1: Tensor = field(metadata={"help": "target samples X_1 (batch_size, ...)."})
|
| 49 |
+
x_0: Tensor = field(metadata={"help": "source samples X_0 (batch_size, ...)."})
|
| 50 |
+
t: Tensor = field(metadata={"help": "time samples t (batch_size, ...)."})
|
| 51 |
+
x_t: Tensor = field(
|
| 52 |
+
metadata={"help": "samples X_t ~ p_t(X_t), shape (batch_size, ...)."}
|
| 53 |
+
)
|
flow_matching/path/scheduler/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
flow_matching/path/scheduler/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
from .schedule_transform import ScheduleTransformedModel
|
| 8 |
+
from .scheduler import (
|
| 9 |
+
CondOTScheduler,
|
| 10 |
+
ConvexScheduler,
|
| 11 |
+
CosineScheduler,
|
| 12 |
+
LinearVPScheduler,
|
| 13 |
+
PolynomialConvexScheduler,
|
| 14 |
+
Scheduler,
|
| 15 |
+
SchedulerOutput,
|
| 16 |
+
VPScheduler,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
__all__ = [
|
| 20 |
+
"CondOTScheduler",
|
| 21 |
+
"CosineScheduler",
|
| 22 |
+
"ConvexScheduler",
|
| 23 |
+
"PolynomialConvexScheduler",
|
| 24 |
+
"ScheduleTransformedModel",
|
| 25 |
+
"Scheduler",
|
| 26 |
+
"VPScheduler",
|
| 27 |
+
"LinearVPScheduler",
|
| 28 |
+
"SchedulerOutput",
|
| 29 |
+
]
|
flow_matching/path/scheduler/schedule_transform.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
from torch import Tensor
|
| 8 |
+
|
| 9 |
+
from flow_matching.path.scheduler.scheduler import Scheduler
|
| 10 |
+
from flow_matching.utils import ModelWrapper
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class ScheduleTransformedModel(ModelWrapper):
|
| 14 |
+
"""
|
| 15 |
+
Change of scheduler for a velocity model.
|
| 16 |
+
|
| 17 |
+
This class wraps a given velocity model and transforms its scheduling
|
| 18 |
+
to a new scheduler function. It modifies the time
|
| 19 |
+
dynamics of the model according to the new scheduler while maintaining
|
| 20 |
+
the original model's behavior.
|
| 21 |
+
|
| 22 |
+
Example:
|
| 23 |
+
|
| 24 |
+
.. code-block:: python
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
from flow_matching.path.scheduler import CondOTScheduler, CosineScheduler, ScheduleTransformedModel
|
| 28 |
+
from flow_matching.solver import ODESolver
|
| 29 |
+
|
| 30 |
+
# Initialize the model and schedulers
|
| 31 |
+
model = ...
|
| 32 |
+
|
| 33 |
+
original_scheduler = CondOTScheduler()
|
| 34 |
+
new_scheduler = CosineScheduler()
|
| 35 |
+
|
| 36 |
+
# Create the transformed model
|
| 37 |
+
transformed_model = ScheduleTransformedModel(
|
| 38 |
+
velocity_model=model,
|
| 39 |
+
original_scheduler=original_scheduler,
|
| 40 |
+
new_scheduler=new_scheduler
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
# Set up the solver
|
| 44 |
+
solver = ODESolver(velocity_model=transformed_model)
|
| 45 |
+
|
| 46 |
+
x_0 = torch.randn([10, 2]) # Example initial condition
|
| 47 |
+
|
| 48 |
+
x_1 = solver.sample(
|
| 49 |
+
time_steps=torch.tensor([0.0, 1.0]),
|
| 50 |
+
x_init=x_0,
|
| 51 |
+
step_size=1/1000
|
| 52 |
+
)[1]
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
velocity_model (ModelWrapper): The original velocity model to be transformed.
|
| 56 |
+
original_scheduler (Scheduler): The scheduler used by the original model. Must implement the snr_inverse function.
|
| 57 |
+
new_scheduler (Scheduler): The new scheduler to be applied to the model.
|
| 58 |
+
"""
|
| 59 |
+
|
| 60 |
+
def __init__(
|
| 61 |
+
self,
|
| 62 |
+
velocity_model: ModelWrapper,
|
| 63 |
+
original_scheduler: Scheduler,
|
| 64 |
+
new_scheduler: Scheduler,
|
| 65 |
+
):
|
| 66 |
+
super().__init__(model=velocity_model)
|
| 67 |
+
self.original_scheduler = original_scheduler
|
| 68 |
+
self.new_scheduler = new_scheduler
|
| 69 |
+
|
| 70 |
+
assert hasattr(self.original_scheduler, "snr_inverse") and callable(
|
| 71 |
+
getattr(self.original_scheduler, "snr_inverse")
|
| 72 |
+
), "The original scheduler must have a callable 'snr_inverse' method."
|
| 73 |
+
|
| 74 |
+
def forward(self, x: Tensor, t: Tensor, **extras) -> Tensor:
|
| 75 |
+
r"""
|
| 76 |
+
Compute the transformed marginal velocity field for a new scheduler.
|
| 77 |
+
This method implements a post-training velocity scheduler change for
|
| 78 |
+
affine conditional flows. It transforms a generating marginal velocity
|
| 79 |
+
field :math:`u_t(x)` based on an original scheduler to a new marginal velocity
|
| 80 |
+
field :math:`\bar{u}_r(x)` based on a different scheduler, while maintaining
|
| 81 |
+
the same data coupling.
|
| 82 |
+
The transformation is based on the scale-time (ST) transformation
|
| 83 |
+
between the two conditional flows, defined as:
|
| 84 |
+
|
| 85 |
+
.. math::
|
| 86 |
+
|
| 87 |
+
\bar{X}_r = s_r X_{t_r},
|
| 88 |
+
|
| 89 |
+
where :math:`X_t` and :math:`\bar{X}_r` are defined by their respective schedulers.
|
| 90 |
+
The ST transformation is computed as:
|
| 91 |
+
|
| 92 |
+
.. math::
|
| 93 |
+
|
| 94 |
+
t_r = \rho^{-1}(\bar{\rho}(r)) \quad \text{and} \quad s_r = \frac{\bar{\sigma}_r}{\sigma_{t_r}}.
|
| 95 |
+
|
| 96 |
+
Here, :math:`\rho(t)` is the signal-to-noise ratio (SNR) defined as:
|
| 97 |
+
|
| 98 |
+
.. math::
|
| 99 |
+
|
| 100 |
+
\rho(t) = \frac{\alpha_t}{\sigma_t}.
|
| 101 |
+
|
| 102 |
+
:math:`\bar{\rho}(r)` is similarly defined for the new scheduler.
|
| 103 |
+
The marginal velocity for the new scheduler is then given by:
|
| 104 |
+
|
| 105 |
+
.. math::
|
| 106 |
+
|
| 107 |
+
\bar{u}_r(x) = \left(\frac{\dot{s}_r}{s_r}\right) x + s_r \dot{t}_r u_{t_r}\left(\frac{x}{s_r}\right).
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
x (Tensor): :math:`x_t`, the input tensor.
|
| 111 |
+
t (Tensor): The time tensor (denoted as :math:`r` above).
|
| 112 |
+
**extras: Additional arguments for the model.
|
| 113 |
+
Returns:
|
| 114 |
+
Tensor: The transformed velocity.
|
| 115 |
+
"""
|
| 116 |
+
r = t
|
| 117 |
+
|
| 118 |
+
r_scheduler_output = self.new_scheduler(t=r)
|
| 119 |
+
|
| 120 |
+
alpha_r = r_scheduler_output.alpha_t
|
| 121 |
+
sigma_r = r_scheduler_output.sigma_t
|
| 122 |
+
d_alpha_r = r_scheduler_output.d_alpha_t
|
| 123 |
+
d_sigma_r = r_scheduler_output.d_sigma_t
|
| 124 |
+
|
| 125 |
+
t = self.original_scheduler.snr_inverse(alpha_r / sigma_r)
|
| 126 |
+
|
| 127 |
+
t_scheduler_output = self.original_scheduler(t=t)
|
| 128 |
+
|
| 129 |
+
alpha_t = t_scheduler_output.alpha_t
|
| 130 |
+
sigma_t = t_scheduler_output.sigma_t
|
| 131 |
+
d_alpha_t = t_scheduler_output.d_alpha_t
|
| 132 |
+
d_sigma_t = t_scheduler_output.d_sigma_t
|
| 133 |
+
|
| 134 |
+
s_r = sigma_r / sigma_t
|
| 135 |
+
|
| 136 |
+
dt_r = (
|
| 137 |
+
sigma_t
|
| 138 |
+
* sigma_t
|
| 139 |
+
* (sigma_r * d_alpha_r - alpha_r * d_sigma_r)
|
| 140 |
+
/ (sigma_r * sigma_r * (sigma_t * d_alpha_t - alpha_t * d_sigma_t))
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
ds_r = (sigma_t * d_sigma_r - sigma_r * d_sigma_t * dt_r) / (sigma_t * sigma_t)
|
| 144 |
+
|
| 145 |
+
u_t = self.model(x=x / s_r, t=t, **extras)
|
| 146 |
+
u_r = ds_r * x / s_r + dt_r * s_r * u_t
|
| 147 |
+
|
| 148 |
+
return u_r
|
flow_matching/path/scheduler/scheduler.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
from abc import ABC, abstractmethod
|
| 8 |
+
from dataclasses import dataclass, field
|
| 9 |
+
|
| 10 |
+
from typing import Union
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
|
| 14 |
+
from torch import Tensor
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class SchedulerOutput:
|
| 19 |
+
r"""Represents a sample of a conditional-flow generated probability path.
|
| 20 |
+
|
| 21 |
+
Attributes:
|
| 22 |
+
alpha_t (Tensor): :math:`\alpha_t`, shape (...).
|
| 23 |
+
sigma_t (Tensor): :math:`\sigma_t`, shape (...).
|
| 24 |
+
d_alpha_t (Tensor): :math:`\frac{\partial}{\partial t}\alpha_t`, shape (...).
|
| 25 |
+
d_sigma_t (Tensor): :math:`\frac{\partial}{\partial t}\sigma_t`, shape (...).
|
| 26 |
+
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
alpha_t: Tensor = field(metadata={"help": "alpha_t"})
|
| 30 |
+
sigma_t: Tensor = field(metadata={"help": "sigma_t"})
|
| 31 |
+
d_alpha_t: Tensor = field(metadata={"help": "Derivative of alpha_t."})
|
| 32 |
+
d_sigma_t: Tensor = field(metadata={"help": "Derivative of sigma_t."})
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class Scheduler(ABC):
|
| 36 |
+
"""Base Scheduler class."""
|
| 37 |
+
|
| 38 |
+
@abstractmethod
|
| 39 |
+
def __call__(self, t: Tensor) -> SchedulerOutput:
|
| 40 |
+
r"""
|
| 41 |
+
Args:
|
| 42 |
+
t (Tensor): times in [0,1], shape (...).
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
SchedulerOutput: :math:`\alpha_t,\sigma_t,\frac{\partial}{\partial t}\alpha_t,\frac{\partial}{\partial t}\sigma_t`
|
| 46 |
+
"""
|
| 47 |
+
...
|
| 48 |
+
|
| 49 |
+
@abstractmethod
|
| 50 |
+
def snr_inverse(self, snr: Tensor) -> Tensor:
|
| 51 |
+
r"""
|
| 52 |
+
Computes :math:`t` from the signal-to-noise ratio :math:`\frac{\alpha_t}{\sigma_t}`.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
snr (Tensor): The signal-to-noise, shape (...)
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
Tensor: t, shape (...)
|
| 59 |
+
"""
|
| 60 |
+
...
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class ConvexScheduler(Scheduler):
|
| 64 |
+
@abstractmethod
|
| 65 |
+
def __call__(self, t: Tensor) -> SchedulerOutput:
|
| 66 |
+
r"""Scheduler for convex paths.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
t (Tensor): times in [0,1], shape (...).
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
SchedulerOutput: :math:`\alpha_t,\sigma_t,\frac{\partial}{\partial t}\alpha_t,\frac{\partial}{\partial t}\sigma_t`
|
| 73 |
+
"""
|
| 74 |
+
...
|
| 75 |
+
|
| 76 |
+
@abstractmethod
|
| 77 |
+
def kappa_inverse(self, kappa: Tensor) -> Tensor:
|
| 78 |
+
r"""
|
| 79 |
+
Computes :math:`t` from :math:`\kappa_t`.
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
kappa (Tensor): :math:`\kappa`, shape (...)
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
Tensor: t, shape (...)
|
| 86 |
+
"""
|
| 87 |
+
...
|
| 88 |
+
|
| 89 |
+
def snr_inverse(self, snr: Tensor) -> Tensor:
|
| 90 |
+
r"""
|
| 91 |
+
Computes :math:`t` from the signal-to-noise ratio :math:`\frac{\alpha_t}{\sigma_t}`.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
snr (Tensor): The signal-to-noise, shape (...)
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
Tensor: t, shape (...)
|
| 98 |
+
"""
|
| 99 |
+
kappa_t = snr / (1.0 + snr)
|
| 100 |
+
|
| 101 |
+
return self.kappa_inverse(kappa=kappa_t)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class CondOTScheduler(ConvexScheduler):
|
| 105 |
+
"""CondOT Scheduler."""
|
| 106 |
+
|
| 107 |
+
def __call__(self, t: Tensor) -> SchedulerOutput:
|
| 108 |
+
return SchedulerOutput(
|
| 109 |
+
alpha_t=t,
|
| 110 |
+
sigma_t=1 - t,
|
| 111 |
+
d_alpha_t=torch.ones_like(t),
|
| 112 |
+
d_sigma_t=-torch.ones_like(t),
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
def kappa_inverse(self, kappa: Tensor) -> Tensor:
|
| 116 |
+
return kappa
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class PolynomialConvexScheduler(ConvexScheduler):
|
| 120 |
+
"""Polynomial Scheduler."""
|
| 121 |
+
|
| 122 |
+
def __init__(self, n: Union[float, int]) -> None:
|
| 123 |
+
assert isinstance(
|
| 124 |
+
n, (float, int)
|
| 125 |
+
), f"`n` must be a float or int. Got {type(n)=}."
|
| 126 |
+
assert n > 0, f"`n` must be positive. Got {n=}."
|
| 127 |
+
|
| 128 |
+
self.n = n
|
| 129 |
+
|
| 130 |
+
def __call__(self, t: Tensor) -> SchedulerOutput:
|
| 131 |
+
return SchedulerOutput(
|
| 132 |
+
alpha_t=t**self.n,
|
| 133 |
+
sigma_t=1 - t**self.n,
|
| 134 |
+
d_alpha_t=self.n * (t ** (self.n - 1)),
|
| 135 |
+
d_sigma_t=-self.n * (t ** (self.n - 1)),
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
def kappa_inverse(self, kappa: Tensor) -> Tensor:
|
| 139 |
+
return torch.pow(kappa, 1.0 / self.n)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class VPScheduler(Scheduler):
|
| 143 |
+
"""Variance Preserving Scheduler."""
|
| 144 |
+
|
| 145 |
+
def __init__(self, beta_min: float = 0.1, beta_max: float = 20.0) -> None:
|
| 146 |
+
self.beta_min = beta_min
|
| 147 |
+
self.beta_max = beta_max
|
| 148 |
+
super().__init__()
|
| 149 |
+
|
| 150 |
+
def __call__(self, t: Tensor) -> SchedulerOutput:
|
| 151 |
+
b = self.beta_min
|
| 152 |
+
B = self.beta_max
|
| 153 |
+
T = 0.5 * (1 - t) ** 2 * (B - b) + (1 - t) * b
|
| 154 |
+
dT = -(1 - t) * (B - b) - b
|
| 155 |
+
|
| 156 |
+
return SchedulerOutput(
|
| 157 |
+
alpha_t=torch.exp(-0.5 * T),
|
| 158 |
+
sigma_t=torch.sqrt(1 - torch.exp(-T)),
|
| 159 |
+
d_alpha_t=-0.5 * dT * torch.exp(-0.5 * T),
|
| 160 |
+
d_sigma_t=0.5 * dT * torch.exp(-T) / torch.sqrt(1 - torch.exp(-T)),
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
def snr_inverse(self, snr: Tensor) -> Tensor:
|
| 164 |
+
T = -torch.log(snr**2 / (snr**2 + 1))
|
| 165 |
+
b = self.beta_min
|
| 166 |
+
B = self.beta_max
|
| 167 |
+
t = 1 - ((-b + torch.sqrt(b**2 + 2 * (B - b) * T)) / (B - b))
|
| 168 |
+
return t
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
class LinearVPScheduler(Scheduler):
|
| 172 |
+
"""Linear Variance Preserving Scheduler."""
|
| 173 |
+
|
| 174 |
+
def __call__(self, t: Tensor) -> SchedulerOutput:
|
| 175 |
+
return SchedulerOutput(
|
| 176 |
+
alpha_t=t,
|
| 177 |
+
sigma_t=(1 - t**2) ** 0.5,
|
| 178 |
+
d_alpha_t=torch.ones_like(t),
|
| 179 |
+
d_sigma_t=-t / (1 - t**2) ** 0.5,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
def snr_inverse(self, snr: Tensor) -> Tensor:
|
| 183 |
+
return torch.sqrt(snr**2 / (1 + snr**2))
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
class CosineScheduler(Scheduler):
|
| 187 |
+
"""Cosine Scheduler."""
|
| 188 |
+
|
| 189 |
+
def __call__(self, t: Tensor) -> SchedulerOutput:
|
| 190 |
+
pi = torch.pi
|
| 191 |
+
return SchedulerOutput(
|
| 192 |
+
alpha_t=torch.sin(pi / 2 * t),
|
| 193 |
+
sigma_t=torch.cos(pi / 2 * t),
|
| 194 |
+
d_alpha_t=pi / 2 * torch.cos(pi / 2 * t),
|
| 195 |
+
d_sigma_t=-pi / 2 * torch.sin(pi / 2 * t),
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
def snr_inverse(self, snr: Tensor) -> Tensor:
|
| 199 |
+
return 2.0 * torch.atan(snr) / torch.pi
|
flow_matching/solver/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
flow_matching/solver/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
from .discrete_solver import MixtureDiscreteEulerSolver
|
| 8 |
+
from .ode_solver import ODESolver
|
| 9 |
+
from .riemannian_ode_solver import RiemannianODESolver
|
| 10 |
+
from .solver import Solver
|
| 11 |
+
from .editflows_solver import EditFlowsEulerSolver
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
"ODESolver",
|
| 15 |
+
"Solver",
|
| 16 |
+
"ModelWrapper",
|
| 17 |
+
"MixtureDiscreteEulerSolver",
|
| 18 |
+
"RiemannianODESolver",
|
| 19 |
+
"EditFlowsEulerSolver",
|
| 20 |
+
]
|
flow_matching/solver/discrete_solver.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
from contextlib import nullcontext
|
| 8 |
+
from math import ceil
|
| 9 |
+
from typing import Callable, Optional, Union
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
from torch import Tensor
|
| 13 |
+
|
| 14 |
+
from torch.nn import functional as F
|
| 15 |
+
|
| 16 |
+
from flow_matching.path import MixtureDiscreteProbPath
|
| 17 |
+
|
| 18 |
+
from flow_matching.solver.solver import Solver
|
| 19 |
+
from flow_matching.utils import categorical, ModelWrapper
|
| 20 |
+
from .utils import get_nearest_times
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
from tqdm import tqdm
|
| 24 |
+
|
| 25 |
+
TQDM_AVAILABLE = True
|
| 26 |
+
except ImportError:
|
| 27 |
+
TQDM_AVAILABLE = False
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class MixtureDiscreteEulerSolver(Solver):
|
| 31 |
+
r"""Solver that simulates the CTMC process :math:`(X_t)_{t_{\text{init}}\leq t\leq t_{\text{final}}}` defined by :math:`p_t` the marginal probability path of ``path``.
|
| 32 |
+
Given :math:`X_t \sim p_t`, the algorithm of solver step from :math:`t` to :math:`t+h` for the i-th coordinate is:
|
| 33 |
+
|
| 34 |
+
.. math::
|
| 35 |
+
|
| 36 |
+
\begin{align*}
|
| 37 |
+
& X_1^i \sim p_{1|t}^i(\cdot|X_t)\\
|
| 38 |
+
& \lambda^i \gets \sum_{x^i\ne X_t^i} u_t^i(x^i, X_t^i|X_1^i)\\
|
| 39 |
+
& Z^i_{\text{change}} \sim U[0,1]\\
|
| 40 |
+
& X_{t+h}^i \sim \begin{cases}
|
| 41 |
+
\frac{u_t^i(\cdot, X_t^i|X_1^i)}{\lambda^i}(1-\delta_{X_t^i}(\cdot)) \text{ if $Z^i_{\text{change}}\le 1-e^{-h\lambda^i}$}\\
|
| 42 |
+
\delta_{X_t^i}(\cdot) \text{ else }
|
| 43 |
+
\end{cases}
|
| 44 |
+
\end{align*}
|
| 45 |
+
|
| 46 |
+
Where :math:`p_{1|t}(\cdot|X_t)` is the output of ``model``, and the conditional probability velocity is of the mixture probability path is:
|
| 47 |
+
|
| 48 |
+
.. math::
|
| 49 |
+
|
| 50 |
+
u_t^i(x^i, y^i|x_1^i) = \hat{u}_t^i(x^i, y^i|x_1^i) + c_{\text{div\_free}}\left[\hat{u}_t^i(x^i, y^i|x_1^i) - \check{u}_t^i(x^i, y^i|x_1^i) \right],
|
| 51 |
+
|
| 52 |
+
where
|
| 53 |
+
|
| 54 |
+
.. math::
|
| 55 |
+
\hat{u}_t^i(x^i, y^i|x_1^i) = \frac{\dot{\kappa}_t}{1-\kappa_t} \left[ \delta_{x_1^i}(x^i) - \delta_{y^i}(x^i) \right],
|
| 56 |
+
|
| 57 |
+
and
|
| 58 |
+
|
| 59 |
+
.. math::
|
| 60 |
+
|
| 61 |
+
\check{u}_t^i(x^i, y^i|x_1^i) = \frac{\dot{\kappa}_t}{\kappa_t}\left[ \delta_{y^i}(x^i) - p(x^i) \right].
|
| 62 |
+
|
| 63 |
+
The source distribution :math:`p(x^i)` is given by ``p``.
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
model (ModelWrapper): trained with x-prediction, outputting posterior probabilities (in the range :math:`[0,1]`), output must be [..., vocabulary_size].
|
| 67 |
+
path (MixtureDiscreteProbPath): Probability path used for x-prediction training.
|
| 68 |
+
vocabulary_size (int): size of the discrete vocabulary.
|
| 69 |
+
source_distribution_p (Optional[Tensor], optional): Source distribution, must be of shape [vocabulary_size]. Required only when divergence-free term for the probability velocity is non-zero. Defaults to None.
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
def __init__(
|
| 73 |
+
self,
|
| 74 |
+
model: ModelWrapper,
|
| 75 |
+
path: MixtureDiscreteProbPath,
|
| 76 |
+
vocabulary_size: int,
|
| 77 |
+
source_distribution_p: Optional[Tensor] = None,
|
| 78 |
+
):
|
| 79 |
+
super().__init__()
|
| 80 |
+
self.model = model
|
| 81 |
+
self.path = path
|
| 82 |
+
self.vocabulary_size = vocabulary_size
|
| 83 |
+
|
| 84 |
+
if source_distribution_p is not None:
|
| 85 |
+
assert source_distribution_p.shape == torch.Size(
|
| 86 |
+
[vocabulary_size]
|
| 87 |
+
), f"Source distribution p dimension must match the vocabulary size {vocabulary_size}. Got {source_distribution_p.shape}."
|
| 88 |
+
|
| 89 |
+
self.source_distribution_p = source_distribution_p
|
| 90 |
+
|
| 91 |
+
@torch.no_grad()
|
| 92 |
+
def sample(
|
| 93 |
+
self,
|
| 94 |
+
x_init: Tensor,
|
| 95 |
+
step_size: Optional[float],
|
| 96 |
+
div_free: Union[float, Callable[[float], float]] = 0.0,
|
| 97 |
+
dtype_categorical: torch.dtype = torch.float32,
|
| 98 |
+
time_grid: Tensor = torch.tensor([0.0, 1.0]),
|
| 99 |
+
return_intermediates: bool = False,
|
| 100 |
+
verbose: bool = False,
|
| 101 |
+
**model_extras,
|
| 102 |
+
) -> Tensor:
|
| 103 |
+
"""
|
| 104 |
+
Sample a sequence of discrete values from the given model.
|
| 105 |
+
|
| 106 |
+
.. code-block:: python
|
| 107 |
+
|
| 108 |
+
import torch
|
| 109 |
+
from flow_matching.utils import ModelWrapper
|
| 110 |
+
from flow_matching.solver import MixtureDiscreteEulerSolver
|
| 111 |
+
|
| 112 |
+
class DummyModel(ModelWrapper):
|
| 113 |
+
def __init__(self):
|
| 114 |
+
super().__init__(None)
|
| 115 |
+
def forward(self, x: torch.Tensor, t: torch.Tensor, **extras) -> torch.Tensor:
|
| 116 |
+
return ...
|
| 117 |
+
|
| 118 |
+
model = DummyModel()
|
| 119 |
+
solver = MixtureDiscreteEulerSolver(model=model)
|
| 120 |
+
|
| 121 |
+
x_init = torch.LongTensor([122, 725])
|
| 122 |
+
step_size = 0.001
|
| 123 |
+
time_grid = torch.tensor([0.0, 1.0])
|
| 124 |
+
|
| 125 |
+
result = solver.sample(x_init=x_init, step_size=step_size, time_grid=time_grid)
|
| 126 |
+
|
| 127 |
+
Args:
|
| 128 |
+
x_init (Tensor): The initial state.
|
| 129 |
+
step_size (Optional[float]): If float then time discretization is uniform with the given step size. If None then time discretization is set to be time_grid.
|
| 130 |
+
div_free (Union[float, Callable[[float], float]]): The coefficient of the divergence-free term in the probability velocity. Can be either a float or a time dependent function. Defaults to 0.0.
|
| 131 |
+
dtype_categorical (torch.dtype): Precision to use for categorical sampler. Defaults to torch.float32.
|
| 132 |
+
time_grid (Tensor): The CTMC process is solved in the interval [time_grid[0], time_grid[-1]] and if step_size is None then time discretization is set by the time grid. Defaults to torch.tensor([0.0,1.0]).
|
| 133 |
+
return_intermediates (bool): If True then return intermediate time steps according to time_grid. Defaults to False.
|
| 134 |
+
verbose (bool): Whether to print progress bars. Defaults to False.
|
| 135 |
+
**model_extras: Additional input for the model.
|
| 136 |
+
|
| 137 |
+
Returns:
|
| 138 |
+
Tensor: The sampled sequence of discrete values.
|
| 139 |
+
|
| 140 |
+
Raises:
|
| 141 |
+
ImportError: To run in verbose mode, tqdm must be installed.
|
| 142 |
+
"""
|
| 143 |
+
if not div_free == 0.0:
|
| 144 |
+
assert (
|
| 145 |
+
self.source_distribution_p is not None
|
| 146 |
+
), "Source distribution p must be specified in order to add a divergence-free term to the probability velocity."
|
| 147 |
+
|
| 148 |
+
# Initialize the current state `x_t` with the initial state `X_0`.
|
| 149 |
+
time_grid = time_grid.to(device=x_init.device)
|
| 150 |
+
|
| 151 |
+
if step_size is None:
|
| 152 |
+
# If step_size is None then set the t discretization to time_grid.
|
| 153 |
+
t_discretization = time_grid
|
| 154 |
+
n_steps = len(time_grid) - 1
|
| 155 |
+
else:
|
| 156 |
+
# If step_size is float then t discretization is uniform with step size set by step_size.
|
| 157 |
+
t_init = time_grid[0].item()
|
| 158 |
+
t_final = time_grid[-1].item()
|
| 159 |
+
assert (
|
| 160 |
+
t_final - t_init
|
| 161 |
+
) > step_size, f"Time interval [time_grid[0], time_grid[-1]] must be larger than step_size. Got a time interval [{t_init}, {t_final}] and step_size {step_size}."
|
| 162 |
+
|
| 163 |
+
n_steps = ceil((t_final - t_init) / step_size)
|
| 164 |
+
t_discretization = torch.tensor(
|
| 165 |
+
[t_init + step_size * i for i in range(n_steps)] + [t_final],
|
| 166 |
+
device=x_init.device,
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
if return_intermediates:
|
| 170 |
+
# get order of intermediate steps:
|
| 171 |
+
order = torch.argsort(time_grid)
|
| 172 |
+
# Compute intermediate steps to return via nearest points in t_discretization to time_grid.
|
| 173 |
+
time_grid = get_nearest_times(
|
| 174 |
+
time_grid=time_grid, t_discretization=t_discretization
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
x_t = x_init.clone()
|
| 178 |
+
steps_counter = 0
|
| 179 |
+
res = []
|
| 180 |
+
|
| 181 |
+
if return_intermediates:
|
| 182 |
+
res = [x_init.clone()]
|
| 183 |
+
|
| 184 |
+
if verbose:
|
| 185 |
+
if not TQDM_AVAILABLE:
|
| 186 |
+
raise ImportError(
|
| 187 |
+
"tqdm is required for verbose mode. Please install it."
|
| 188 |
+
)
|
| 189 |
+
ctx = tqdm(total=t_final, desc=f"NFE: {steps_counter}")
|
| 190 |
+
else:
|
| 191 |
+
ctx = nullcontext()
|
| 192 |
+
|
| 193 |
+
with ctx:
|
| 194 |
+
for i in range(n_steps):
|
| 195 |
+
t = t_discretization[i : i + 1]
|
| 196 |
+
h = t_discretization[i + 1 : i + 2] - t_discretization[i : i + 1]
|
| 197 |
+
|
| 198 |
+
# Sample x_1 ~ p_1|t( \cdot |x_t)
|
| 199 |
+
p_1t = self.model(x=x_t, t=t.repeat(x_t.shape[0]), **model_extras)
|
| 200 |
+
x_1 = categorical(p_1t.to(dtype=dtype_categorical))
|
| 201 |
+
|
| 202 |
+
# Checks if final step
|
| 203 |
+
if i == n_steps - 1:
|
| 204 |
+
x_t = x_1
|
| 205 |
+
else:
|
| 206 |
+
# Compute u_t(x|x_t,x_1)
|
| 207 |
+
scheduler_output = self.path.scheduler(t=t)
|
| 208 |
+
|
| 209 |
+
k_t = scheduler_output.alpha_t
|
| 210 |
+
d_k_t = scheduler_output.d_alpha_t
|
| 211 |
+
|
| 212 |
+
delta_1 = F.one_hot(x_1, num_classes=self.vocabulary_size).to(
|
| 213 |
+
k_t.dtype
|
| 214 |
+
)
|
| 215 |
+
u = d_k_t / (1 - k_t) * delta_1
|
| 216 |
+
|
| 217 |
+
# Add divergence-free part
|
| 218 |
+
div_free_t = div_free(t) if callable(div_free) else div_free
|
| 219 |
+
|
| 220 |
+
if div_free_t > 0:
|
| 221 |
+
p_0 = self.source_distribution_p[(None,) * x_t.dim()]
|
| 222 |
+
u = u + div_free_t * d_k_t / (k_t * (1 - k_t)) * (
|
| 223 |
+
(1 - k_t) * p_0 + k_t * delta_1
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
# Set u_t(x_t|x_t,x_1) = 0
|
| 227 |
+
delta_t = F.one_hot(x_t, num_classes=self.vocabulary_size)
|
| 228 |
+
u = torch.where(
|
| 229 |
+
delta_t.to(dtype=torch.bool), torch.zeros_like(u), u
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
# Sample x_t ~ u_t( \cdot |x_t,x_1)
|
| 233 |
+
intensity = u.sum(dim=-1) # Assuming u_t(xt|xt,x1) := 0
|
| 234 |
+
mask_jump = torch.rand(
|
| 235 |
+
size=x_t.shape, device=x_t.device
|
| 236 |
+
) < 1 - torch.exp(-h * intensity)
|
| 237 |
+
|
| 238 |
+
if mask_jump.sum() > 0:
|
| 239 |
+
x_t[mask_jump] = categorical(
|
| 240 |
+
u[mask_jump].to(dtype=dtype_categorical)
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
steps_counter += 1
|
| 244 |
+
t = t + h
|
| 245 |
+
|
| 246 |
+
if return_intermediates and (t in time_grid):
|
| 247 |
+
res.append(x_t.clone())
|
| 248 |
+
|
| 249 |
+
if verbose:
|
| 250 |
+
ctx.n = t.item()
|
| 251 |
+
ctx.refresh()
|
| 252 |
+
ctx.set_description(f"NFE: {steps_counter}")
|
| 253 |
+
|
| 254 |
+
if return_intermediates:
|
| 255 |
+
if step_size is None:
|
| 256 |
+
return torch.stack(res, dim=0)
|
| 257 |
+
else:
|
| 258 |
+
return torch.stack(res, dim=0)[order]
|
| 259 |
+
else:
|
| 260 |
+
return x_t
|
flow_matching/solver/editflows_solver.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
|
| 4 |
+
from typing import Callable, List, Sequence, Tuple, Optional
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class EditFlowsEulerSolver:
|
| 12 |
+
"""
|
| 13 |
+
Ragged EditFlows CTMC solver with Euler thinning (≤1 jump per step).
|
| 14 |
+
|
| 15 |
+
At each step h:
|
| 16 |
+
- query model for (λ_ins[slots], Q_ins[slots], λ_del[tokens], λ_sub[tokens], Q_sub[tokens])
|
| 17 |
+
- total intensity Λ = sum(λ_ins) + sum(λ_del) + sum(λ_sub)
|
| 18 |
+
- with prob 1 - exp(-h Λ), take ONE jump:
|
| 19 |
+
* sample which event from concatenated rates
|
| 20 |
+
* if ins/sub: sample token from the Q at that position/slot
|
| 21 |
+
* apply edit to the ragged sequence (insert/delete/replace)
|
| 22 |
+
- advance t ← t + h
|
| 23 |
+
|
| 24 |
+
This is faithful to the CTMC in the small-h regime used by Euler schemes.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
def __init__(
|
| 28 |
+
self,
|
| 29 |
+
model, # callable: (x_list, t_vec) -> ragged heads
|
| 30 |
+
scheduler: Callable[[Tensor], object], # path.scheduler(t) providing alpha_t, d_alpha_t, sigma_t if needed
|
| 31 |
+
vocab_size: int,
|
| 32 |
+
dtype_categorical: torch.dtype = torch.float64,
|
| 33 |
+
generator: Optional[torch.Generator] = None,
|
| 34 |
+
) -> None:
|
| 35 |
+
self.model = model
|
| 36 |
+
self.scheduler = scheduler
|
| 37 |
+
self.vocab_size = vocab_size
|
| 38 |
+
self.dtype_cat = dtype_categorical
|
| 39 |
+
self.gen = generator
|
| 40 |
+
|
| 41 |
+
@torch.no_grad()
|
| 42 |
+
def _sample_categorical_logits(self, logits: Tensor) -> Tensor:
|
| 43 |
+
# logits: (V,) -> sample id
|
| 44 |
+
probs = F.softmax(logits, dim=-1, dtype=self.dtype_cat)
|
| 45 |
+
return torch.multinomial(probs, num_samples=1, replacement=True, generator=self.gen).squeeze(0)
|
| 46 |
+
|
| 47 |
+
@torch.no_grad()
|
| 48 |
+
def _maybe_one_jump(
|
| 49 |
+
self,
|
| 50 |
+
x: Tensor, # (n,)
|
| 51 |
+
lam_ins: Tensor, # (n+1,)
|
| 52 |
+
logits_ins: Tensor, # (n+1, V)
|
| 53 |
+
lam_del: Tensor, # (n,)
|
| 54 |
+
lam_sub: Tensor, # (n,)
|
| 55 |
+
logits_sub: Tensor, # (n, V)
|
| 56 |
+
h: float,
|
| 57 |
+
device: torch.device,
|
| 58 |
+
) -> Tensor:
|
| 59 |
+
"""
|
| 60 |
+
Apply at most one edit event to x with step size h.
|
| 61 |
+
Returns possibly modified x (1-D LongTensor).
|
| 62 |
+
"""
|
| 63 |
+
n = x.numel()
|
| 64 |
+
# Total intensity
|
| 65 |
+
sum_ins = lam_ins.sum() if lam_ins.numel() else torch.tensor(0.0, device=device)
|
| 66 |
+
sum_del = lam_del.sum() if lam_del.numel() else torch.tensor(0.0, device=device)
|
| 67 |
+
sum_sub = lam_sub.sum() if lam_sub.numel() else torch.tensor(0.0, device=device)
|
| 68 |
+
total = (sum_ins + sum_del + sum_sub).clamp(min=0.0)
|
| 69 |
+
|
| 70 |
+
if total.item() <= 0:
|
| 71 |
+
return x # no possible jumps
|
| 72 |
+
|
| 73 |
+
# Bernoulli: does a jump occur?
|
| 74 |
+
p_jump = 1.0 - torch.exp(torch.tensor(-h, device=device) * total)
|
| 75 |
+
if torch.rand((), device=device, generator=self.gen) >= p_jump:
|
| 76 |
+
return x # no jump this step
|
| 77 |
+
|
| 78 |
+
# Build event vector: [ins(0..n), del(0..n-1), sub(0..n-1)]
|
| 79 |
+
parts = []
|
| 80 |
+
if lam_ins.numel(): parts.append(lam_ins)
|
| 81 |
+
if lam_del.numel(): parts.append(lam_del)
|
| 82 |
+
if lam_sub.numel(): parts.append(lam_sub)
|
| 83 |
+
lam_cat = torch.cat(parts, dim=0) # (n+1 + n + n,)
|
| 84 |
+
probs = (lam_cat / total).to(self.dtype_cat)
|
| 85 |
+
|
| 86 |
+
# Sample which event
|
| 87 |
+
e_idx = torch.multinomial(probs, num_samples=1, generator=self.gen).item()
|
| 88 |
+
|
| 89 |
+
# Decode which branch
|
| 90 |
+
offset = 0
|
| 91 |
+
if e_idx < lam_ins.numel():
|
| 92 |
+
# INSERT at slot s
|
| 93 |
+
s = e_idx
|
| 94 |
+
# sample token from slot distribution
|
| 95 |
+
y = self._sample_categorical_logits(logits_ins[s]) # id ∈ [0..V-1]
|
| 96 |
+
# perform insertion at slot s: x' = [x[:s], y, x[s:]]
|
| 97 |
+
if s == 0:
|
| 98 |
+
x_new = torch.cat([y.view(1), x], dim=0)
|
| 99 |
+
elif s == n:
|
| 100 |
+
x_new = torch.cat([x, y.view(1)], dim=0)
|
| 101 |
+
else:
|
| 102 |
+
x_new = torch.cat([x[:s], y.view(1), x[s:]], dim=0)
|
| 103 |
+
return x_new
|
| 104 |
+
|
| 105 |
+
e_idx -= lam_ins.numel()
|
| 106 |
+
if e_idx < lam_del.numel():
|
| 107 |
+
# DELETE token j
|
| 108 |
+
j = e_idx
|
| 109 |
+
if n == 0:
|
| 110 |
+
return x
|
| 111 |
+
if j == 0:
|
| 112 |
+
return x[1:]
|
| 113 |
+
elif j == n - 1:
|
| 114 |
+
return x[:-1]
|
| 115 |
+
else:
|
| 116 |
+
return torch.cat([x[:j], x[j + 1 :]], dim=0)
|
| 117 |
+
|
| 118 |
+
e_idx -= lam_del.numel()
|
| 119 |
+
# SUBSTITUTE at token j
|
| 120 |
+
j = e_idx
|
| 121 |
+
if n == 0:
|
| 122 |
+
return x
|
| 123 |
+
y = self._sample_categorical_logits(logits_sub[j])
|
| 124 |
+
x_new = x.clone()
|
| 125 |
+
x_new[j] = y
|
| 126 |
+
return x_new
|
| 127 |
+
|
| 128 |
+
@torch.no_grad()
|
| 129 |
+
def sample(
|
| 130 |
+
self,
|
| 131 |
+
x_list: Sequence[Tensor], # ragged: list[(n_i,)]
|
| 132 |
+
n_steps: int,
|
| 133 |
+
t0: float = 0.0,
|
| 134 |
+
t1: float = 1.0,
|
| 135 |
+
verbose: bool = False,
|
| 136 |
+
) -> List[Tensor]:
|
| 137 |
+
"""
|
| 138 |
+
Evolve all sequences independently with shared time grid t_k.
|
| 139 |
+
"""
|
| 140 |
+
assert n_steps >= 1
|
| 141 |
+
B = len(x_list)
|
| 142 |
+
device = x_list[0].device if B > 0 else torch.device("cpu")
|
| 143 |
+
|
| 144 |
+
x = [xi.clone() for xi in x_list]
|
| 145 |
+
t = torch.full((B,), float(t0), device=device)
|
| 146 |
+
h = (t1 - t0) / float(n_steps)
|
| 147 |
+
|
| 148 |
+
for k in range(n_steps):
|
| 149 |
+
# Query model once at time t_k (vector of size B)
|
| 150 |
+
lam_ins, logits_ins, lam_del, lam_sub, logits_sub = self.model(x, t)
|
| 151 |
+
|
| 152 |
+
# Apply at most one jump per sample
|
| 153 |
+
for i in range(B):
|
| 154 |
+
x[i] = self._maybe_one_jump(
|
| 155 |
+
x=x[i],
|
| 156 |
+
lam_ins=lam_ins[i].squeeze(0) if lam_ins[i].dim() == 2 else lam_ins[i],
|
| 157 |
+
logits_ins=logits_ins[i].squeeze(0) if logits_ins[i].dim() == 3 else logits_ins[i],
|
| 158 |
+
lam_del=lam_del[i].squeeze(0) if lam_del[i].dim() == 2 else lam_del[i],
|
| 159 |
+
lam_sub=lam_sub[i].squeeze(0) if lam_sub[i].dim() == 2 else lam_sub[i],
|
| 160 |
+
logits_sub=logits_sub[i].squeeze(0) if logits_sub[i].dim() == 3 else logits_sub[i],
|
| 161 |
+
h=h,
|
| 162 |
+
device=device,
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
# advance time
|
| 166 |
+
t = t + h
|
| 167 |
+
|
| 168 |
+
return x
|
flow_matching/solver/ode_solver.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
from typing import Callable, Optional, Sequence, Tuple, Union
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from torch import Tensor
|
| 11 |
+
from torchdiffeq import odeint
|
| 12 |
+
|
| 13 |
+
from flow_matching.solver.solver import Solver
|
| 14 |
+
from flow_matching.utils import gradient, ModelWrapper
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ODESolver(Solver):
|
| 18 |
+
"""A class to solve ordinary differential equations (ODEs) using a specified velocity model.
|
| 19 |
+
|
| 20 |
+
This class utilizes a velocity field model to solve ODEs over a given time grid using numerical ode solvers.
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
velocity_model (Union[ModelWrapper, Callable]): a velocity field model receiving :math:`(x,t)` and returning :math:`u_t(x)`
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, velocity_model: Union[ModelWrapper, Callable]):
|
| 27 |
+
super().__init__()
|
| 28 |
+
self.velocity_model = velocity_model
|
| 29 |
+
|
| 30 |
+
def sample(
|
| 31 |
+
self,
|
| 32 |
+
x_init: Tensor,
|
| 33 |
+
step_size: Optional[float],
|
| 34 |
+
method: str = "euler",
|
| 35 |
+
atol: float = 1e-5,
|
| 36 |
+
rtol: float = 1e-5,
|
| 37 |
+
time_grid: Tensor = torch.tensor([0.0, 1.0]),
|
| 38 |
+
return_intermediates: bool = False,
|
| 39 |
+
enable_grad: bool = False,
|
| 40 |
+
**model_extras,
|
| 41 |
+
) -> Union[Tensor, Sequence[Tensor]]:
|
| 42 |
+
r"""Solve the ODE with the velocity field.
|
| 43 |
+
|
| 44 |
+
Example:
|
| 45 |
+
|
| 46 |
+
.. code-block:: python
|
| 47 |
+
|
| 48 |
+
import torch
|
| 49 |
+
from flow_matching.utils import ModelWrapper
|
| 50 |
+
from flow_matching.solver import ODESolver
|
| 51 |
+
|
| 52 |
+
class DummyModel(ModelWrapper):
|
| 53 |
+
def __init__(self):
|
| 54 |
+
super().__init__(None)
|
| 55 |
+
|
| 56 |
+
def forward(self, x: torch.Tensor, t: torch.Tensor, **extras) -> torch.Tensor:
|
| 57 |
+
return torch.ones_like(x) * 3.0 * t**2
|
| 58 |
+
|
| 59 |
+
velocity_model = DummyModel()
|
| 60 |
+
solver = ODESolver(velocity_model=velocity_model)
|
| 61 |
+
x_init = torch.tensor([0.0, 0.0])
|
| 62 |
+
step_size = 0.001
|
| 63 |
+
time_grid = torch.tensor([0.0, 1.0])
|
| 64 |
+
|
| 65 |
+
result = solver.sample(x_init=x_init, step_size=step_size, time_grid=time_grid)
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
x_init (Tensor): initial conditions (e.g., source samples :math:`X_0 \sim p`). Shape: [batch_size, ...].
|
| 69 |
+
step_size (Optional[float]): The step size. Must be None for adaptive step solvers.
|
| 70 |
+
method (str): A method supported by torchdiffeq. Defaults to "euler". Other commonly used solvers are "dopri5", "midpoint" and "heun3". For a complete list, see torchdiffeq.
|
| 71 |
+
atol (float): Absolute tolerance, used for adaptive step solvers.
|
| 72 |
+
rtol (float): Relative tolerance, used for adaptive step solvers.
|
| 73 |
+
time_grid (Tensor): The process is solved in the interval [min(time_grid, max(time_grid)] and if step_size is None then time discretization is set by the time grid. May specify a descending time_grid to solve in the reverse direction. Defaults to torch.tensor([0.0, 1.0]).
|
| 74 |
+
return_intermediates (bool, optional): If True then return intermediate time steps according to time_grid. Defaults to False.
|
| 75 |
+
enable_grad (bool, optional): Whether to compute gradients during sampling. Defaults to False.
|
| 76 |
+
**model_extras: Additional input for the model.
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
Union[Tensor, Sequence[Tensor]]: The last timestep when return_intermediates=False, otherwise all values specified in time_grid.
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
time_grid = time_grid.to(x_init.device)
|
| 83 |
+
|
| 84 |
+
def ode_func(t, x):
|
| 85 |
+
return self.velocity_model(x=x, t=t, **model_extras)
|
| 86 |
+
|
| 87 |
+
ode_opts = {"step_size": step_size} if step_size is not None else {}
|
| 88 |
+
|
| 89 |
+
with torch.set_grad_enabled(enable_grad):
|
| 90 |
+
# Approximate ODE solution with numerical ODE solver
|
| 91 |
+
sol = odeint(
|
| 92 |
+
ode_func,
|
| 93 |
+
x_init,
|
| 94 |
+
time_grid,
|
| 95 |
+
method=method,
|
| 96 |
+
options=ode_opts,
|
| 97 |
+
atol=atol,
|
| 98 |
+
rtol=rtol,
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
if return_intermediates:
|
| 102 |
+
return sol
|
| 103 |
+
else:
|
| 104 |
+
return sol[-1]
|
| 105 |
+
|
| 106 |
+
def compute_likelihood(
|
| 107 |
+
self,
|
| 108 |
+
x_1: Tensor,
|
| 109 |
+
log_p0: Callable[[Tensor], Tensor],
|
| 110 |
+
step_size: Optional[float],
|
| 111 |
+
method: str = "euler",
|
| 112 |
+
atol: float = 1e-5,
|
| 113 |
+
rtol: float = 1e-5,
|
| 114 |
+
time_grid: Tensor = torch.tensor([1.0, 0.0]),
|
| 115 |
+
return_intermediates: bool = False,
|
| 116 |
+
exact_divergence: bool = False,
|
| 117 |
+
enable_grad: bool = False,
|
| 118 |
+
**model_extras,
|
| 119 |
+
) -> Union[Tuple[Tensor, Tensor], Tuple[Sequence[Tensor], Tensor]]:
|
| 120 |
+
r"""Solve for log likelihood given a target sample at :math:`t=0`.
|
| 121 |
+
|
| 122 |
+
Works similarly to sample, but solves the ODE in reverse to compute the log-likelihood. The velocity model must be differentiable with respect to x.
|
| 123 |
+
The function assumes log_p0 is the log probability of the source distribution at :math:`t=0`.
|
| 124 |
+
|
| 125 |
+
Args:
|
| 126 |
+
x_1 (Tensor): target sample (e.g., samples :math:`X_1 \sim p_1`).
|
| 127 |
+
log_p0 (Callable[[Tensor], Tensor]): Log probability function of the source distribution.
|
| 128 |
+
step_size (Optional[float]): The step size. Must be None for adaptive step solvers.
|
| 129 |
+
method (str): A method supported by torchdiffeq. Defaults to "euler". Other commonly used solvers are "dopri5", "midpoint" and "heun3". For a complete list, see torchdiffeq.
|
| 130 |
+
atol (float): Absolute tolerance, used for adaptive step solvers.
|
| 131 |
+
rtol (float): Relative tolerance, used for adaptive step solvers.
|
| 132 |
+
time_grid (Tensor): If step_size is None then time discretization is set by the time grid. Must start at 1.0 and end at 0.0, otherwise the likelihood computation is not valid. Defaults to torch.tensor([1.0, 0.0]).
|
| 133 |
+
return_intermediates (bool, optional): If True then return intermediate time steps according to time_grid. Otherwise only return the final sample. Defaults to False.
|
| 134 |
+
exact_divergence (bool): Whether to compute the exact divergence or use the Hutchinson estimator.
|
| 135 |
+
enable_grad (bool, optional): Whether to compute gradients during sampling. Defaults to False.
|
| 136 |
+
**model_extras: Additional input for the model.
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
Union[Tuple[Tensor, Tensor], Tuple[Sequence[Tensor], Tensor]]: Samples at time_grid and log likelihood values of given x_1.
|
| 140 |
+
"""
|
| 141 |
+
assert (
|
| 142 |
+
time_grid[0] == 1.0 and time_grid[-1] == 0.0
|
| 143 |
+
), f"Time grid must start at 1.0 and end at 0.0. Got {time_grid}"
|
| 144 |
+
|
| 145 |
+
# Fix the random projection for the Hutchinson divergence estimator
|
| 146 |
+
if not exact_divergence:
|
| 147 |
+
z = (torch.randn_like(x_1).to(x_1.device) < 0) * 2.0 - 1.0
|
| 148 |
+
|
| 149 |
+
def ode_func(x, t):
|
| 150 |
+
return self.velocity_model(x=x, t=t, **model_extras)
|
| 151 |
+
|
| 152 |
+
def dynamics_func(t, states):
|
| 153 |
+
xt = states[0]
|
| 154 |
+
with torch.set_grad_enabled(True):
|
| 155 |
+
xt.requires_grad_()
|
| 156 |
+
ut = ode_func(xt, t)
|
| 157 |
+
|
| 158 |
+
if exact_divergence:
|
| 159 |
+
# Compute exact divergence
|
| 160 |
+
div = 0
|
| 161 |
+
for i in range(ut.flatten(1).shape[1]):
|
| 162 |
+
div += gradient(ut[:, i], xt, create_graph=True)[:, i].detach()
|
| 163 |
+
else:
|
| 164 |
+
# Compute Hutchinson divergence estimator E[z^T D_x(ut) z]
|
| 165 |
+
ut_dot_z = torch.einsum(
|
| 166 |
+
"ij,ij->i", ut.flatten(start_dim=1), z.flatten(start_dim=1)
|
| 167 |
+
)
|
| 168 |
+
grad_ut_dot_z = gradient(ut_dot_z, xt)
|
| 169 |
+
div = torch.einsum(
|
| 170 |
+
"ij,ij->i",
|
| 171 |
+
grad_ut_dot_z.flatten(start_dim=1),
|
| 172 |
+
z.flatten(start_dim=1),
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
return ut.detach(), div.detach()
|
| 176 |
+
|
| 177 |
+
y_init = (x_1, torch.zeros(x_1.shape[0], device=x_1.device))
|
| 178 |
+
ode_opts = {"step_size": step_size} if step_size is not None else {}
|
| 179 |
+
|
| 180 |
+
with torch.set_grad_enabled(enable_grad):
|
| 181 |
+
sol, log_det = odeint(
|
| 182 |
+
dynamics_func,
|
| 183 |
+
y_init,
|
| 184 |
+
time_grid,
|
| 185 |
+
method=method,
|
| 186 |
+
options=ode_opts,
|
| 187 |
+
atol=atol,
|
| 188 |
+
rtol=rtol,
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
x_source = sol[-1]
|
| 192 |
+
source_log_p = log_p0(x_source)
|
| 193 |
+
|
| 194 |
+
if return_intermediates:
|
| 195 |
+
return sol, source_log_p + log_det[-1]
|
| 196 |
+
else:
|
| 197 |
+
return sol[-1], source_log_p + log_det[-1]
|
flow_matching/solver/riemannian_ode_solver.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
import math
|
| 8 |
+
from typing import Callable
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from torch import Tensor
|
| 12 |
+
|
| 13 |
+
from flow_matching.solver.solver import Solver
|
| 14 |
+
from flow_matching.utils import ModelWrapper
|
| 15 |
+
from flow_matching.utils.manifolds import geodesic, Manifold
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
from tqdm import tqdm
|
| 19 |
+
|
| 20 |
+
TQDM_AVAILABLE = True
|
| 21 |
+
except ImportError:
|
| 22 |
+
TQDM_AVAILABLE = False
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class RiemannianODESolver(Solver):
|
| 26 |
+
r"""Riemannian ODE solver
|
| 27 |
+
Initialize the ``RiemannianODESolver``.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
manifold (Manifold): the manifold to solve on.
|
| 31 |
+
velocity_model (ModelWrapper): a velocity field model receiving :math:`(x,t)`
|
| 32 |
+
and returning :math:`u_t(x)` which is assumed to lie on the tangent plane at `x`.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def __init__(self, manifold: Manifold, velocity_model: ModelWrapper):
|
| 36 |
+
super().__init__()
|
| 37 |
+
self.manifold = manifold
|
| 38 |
+
self.velocity_model = velocity_model
|
| 39 |
+
|
| 40 |
+
def sample(
|
| 41 |
+
self,
|
| 42 |
+
x_init: Tensor,
|
| 43 |
+
step_size: float,
|
| 44 |
+
projx: bool = True,
|
| 45 |
+
proju: bool = True,
|
| 46 |
+
method: str = "euler",
|
| 47 |
+
time_grid: Tensor = torch.tensor([0.0, 1.0]),
|
| 48 |
+
return_intermediates: bool = False,
|
| 49 |
+
verbose: bool = False,
|
| 50 |
+
enable_grad: bool = False,
|
| 51 |
+
**model_extras,
|
| 52 |
+
) -> Tensor:
|
| 53 |
+
r"""Solve the ODE with the `velocity_field` on the manifold.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
x_init (Tensor): initial conditions (e.g., source samples :math:`X_0 \sim p`).
|
| 57 |
+
step_size (float): The step size.
|
| 58 |
+
projx (bool): Whether to project the point onto the manifold at each step. Defaults to True.
|
| 59 |
+
proju (bool): Whether to project the vector field onto the tangent plane at each step. Defaults to True.
|
| 60 |
+
method (str): One of ["euler", "midpoint", "rk4"]. Defaults to "euler".
|
| 61 |
+
time_grid (Tensor, optional): The process is solved in the interval [min(time_grid, max(time_grid)] and if step_size is None then time discretization is set by the time grid. Defaults to torch.tensor([0.0,1.0]).
|
| 62 |
+
return_intermediates (bool, optional): If True then return intermediate time steps according to time_grid. Defaults to False.
|
| 63 |
+
verbose (bool, optional): Whether to print progress bars. Defaults to False.
|
| 64 |
+
enable_grad (bool, optional): Whether to compute gradients during sampling. Defaults to False.
|
| 65 |
+
**model_extras: Additional input for the model.
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
Tensor: The sampled sequence. Defaults to returning samples at :math:`t=1`.
|
| 69 |
+
|
| 70 |
+
Raises:
|
| 71 |
+
ImportError: To run in verbose mode, tqdm must be installed.
|
| 72 |
+
"""
|
| 73 |
+
step_fns = {
|
| 74 |
+
"euler": _euler_step,
|
| 75 |
+
"midpoint": _midpoint_step,
|
| 76 |
+
"rk4": _rk4_step,
|
| 77 |
+
}
|
| 78 |
+
assert method in step_fns.keys(), f"Unknown method {method}"
|
| 79 |
+
step_fn = step_fns[method]
|
| 80 |
+
|
| 81 |
+
def velocity_func(x, t):
|
| 82 |
+
return self.velocity_model(x=x, t=t, **model_extras)
|
| 83 |
+
|
| 84 |
+
# --- Factor this out.
|
| 85 |
+
time_grid = torch.sort(time_grid.to(device=x_init.device)).values
|
| 86 |
+
|
| 87 |
+
if step_size is None:
|
| 88 |
+
# If step_size is None then set the t discretization to time_grid.
|
| 89 |
+
t_discretization = time_grid
|
| 90 |
+
n_steps = len(time_grid) - 1
|
| 91 |
+
else:
|
| 92 |
+
# If step_size is float then t discretization is uniform with step size set by step_size.
|
| 93 |
+
t_init = time_grid[0].item()
|
| 94 |
+
t_final = time_grid[-1].item()
|
| 95 |
+
assert (
|
| 96 |
+
t_final - t_init
|
| 97 |
+
) > step_size, f"Time interval [min(time_grid), max(time_grid)] must be larger than step_size. Got a time interval [{t_init}, {t_final}] and step_size {step_size}."
|
| 98 |
+
|
| 99 |
+
n_steps = math.ceil((t_final - t_init) / step_size)
|
| 100 |
+
t_discretization = torch.tensor(
|
| 101 |
+
[step_size * i for i in range(n_steps)] + [t_final],
|
| 102 |
+
device=x_init.device,
|
| 103 |
+
)
|
| 104 |
+
# ---
|
| 105 |
+
t0s = t_discretization[:-1]
|
| 106 |
+
|
| 107 |
+
if verbose:
|
| 108 |
+
if not TQDM_AVAILABLE:
|
| 109 |
+
raise ImportError(
|
| 110 |
+
"tqdm is required for verbose mode. Please install it."
|
| 111 |
+
)
|
| 112 |
+
t0s = tqdm(t0s)
|
| 113 |
+
|
| 114 |
+
if return_intermediates:
|
| 115 |
+
xts = []
|
| 116 |
+
i_ret = 0
|
| 117 |
+
|
| 118 |
+
with torch.set_grad_enabled(enable_grad):
|
| 119 |
+
xt = x_init
|
| 120 |
+
for t0, t1 in zip(t0s, t_discretization[1:]):
|
| 121 |
+
dt = t1 - t0
|
| 122 |
+
xt_next = step_fn(
|
| 123 |
+
velocity_func,
|
| 124 |
+
xt,
|
| 125 |
+
t0,
|
| 126 |
+
dt,
|
| 127 |
+
manifold=self.manifold,
|
| 128 |
+
projx=projx,
|
| 129 |
+
proju=proju,
|
| 130 |
+
)
|
| 131 |
+
if return_intermediates:
|
| 132 |
+
while (
|
| 133 |
+
i_ret < len(time_grid)
|
| 134 |
+
and t0 <= time_grid[i_ret]
|
| 135 |
+
and time_grid[i_ret] <= t1
|
| 136 |
+
):
|
| 137 |
+
xts.append(
|
| 138 |
+
interp(self.manifold, xt, xt_next, t0, t1, time_grid[i_ret])
|
| 139 |
+
)
|
| 140 |
+
i_ret += 1
|
| 141 |
+
xt = xt_next
|
| 142 |
+
|
| 143 |
+
if return_intermediates:
|
| 144 |
+
return torch.stack(xts, dim=0)
|
| 145 |
+
else:
|
| 146 |
+
return xt
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def interp(manifold, xt, xt_next, t, t_next, t_ret):
|
| 150 |
+
return geodesic(manifold, xt, xt_next)(
|
| 151 |
+
(t_ret - t) / (t_next - t).reshape(1)
|
| 152 |
+
).reshape_as(xt)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def _euler_step(
|
| 156 |
+
velocity_model: Callable,
|
| 157 |
+
xt: Tensor,
|
| 158 |
+
t0: Tensor,
|
| 159 |
+
dt: Tensor,
|
| 160 |
+
manifold: Manifold,
|
| 161 |
+
projx: bool = True,
|
| 162 |
+
proju: bool = True,
|
| 163 |
+
) -> Tensor:
|
| 164 |
+
r"""Perform an Euler step on a manifold.
|
| 165 |
+
|
| 166 |
+
Args:
|
| 167 |
+
velocity_model (Callable): the velocity model
|
| 168 |
+
xt (Tensor): tensor containing the state at time t0
|
| 169 |
+
t0 (Tensor): the time at which this step is taken
|
| 170 |
+
dt (Tensor): the step size
|
| 171 |
+
manifold (Manifold): a manifold object
|
| 172 |
+
projx (bool, optional): whether to project the state onto the manifold. Defaults to True.
|
| 173 |
+
proju (bool, optional): whether to project the velocity onto the tangent plane. Defaults to True.
|
| 174 |
+
|
| 175 |
+
Returns:
|
| 176 |
+
Tensor: tensor containing the state after the step
|
| 177 |
+
"""
|
| 178 |
+
velocity_fn = lambda x, t: (
|
| 179 |
+
manifold.proju(x, velocity_model(x, t)) if proju else velocity_model(x, t)
|
| 180 |
+
)
|
| 181 |
+
projx_fn = lambda x: manifold.projx(x) if projx else x
|
| 182 |
+
|
| 183 |
+
vt = velocity_fn(xt, t0)
|
| 184 |
+
|
| 185 |
+
xt = xt + dt * vt
|
| 186 |
+
|
| 187 |
+
return projx_fn(xt)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def _midpoint_step(
|
| 191 |
+
velocity_model: Callable,
|
| 192 |
+
xt: Tensor,
|
| 193 |
+
t0: Tensor,
|
| 194 |
+
dt: Tensor,
|
| 195 |
+
manifold: Manifold,
|
| 196 |
+
projx: bool = True,
|
| 197 |
+
proju: bool = True,
|
| 198 |
+
) -> Tensor:
|
| 199 |
+
r"""Perform a midpoint step on a manifold.
|
| 200 |
+
|
| 201 |
+
Args:
|
| 202 |
+
velocity_model (Callable): the velocity model
|
| 203 |
+
xt (Tensor): tensor containing the state at time t0
|
| 204 |
+
t0 (Tensor): the time at which this step is taken
|
| 205 |
+
dt (Tensor): the step size
|
| 206 |
+
manifold (Manifold): a manifold object
|
| 207 |
+
projx (bool, optional): whether to project the state onto the manifold. Defaults to True.
|
| 208 |
+
proju (bool, optional): whether to project the velocity onto the tangent plane. Defaults to True.
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
Tensor: tensor containing the state after the step
|
| 212 |
+
"""
|
| 213 |
+
velocity_fn = lambda x, t: (
|
| 214 |
+
manifold.proju(x, velocity_model(x, t)) if proju else velocity_model(x, t)
|
| 215 |
+
)
|
| 216 |
+
projx_fn = lambda x: manifold.projx(x) if projx else x
|
| 217 |
+
|
| 218 |
+
half_dt = 0.5 * dt
|
| 219 |
+
vt = velocity_fn(xt, t0)
|
| 220 |
+
x_mid = xt + half_dt * vt
|
| 221 |
+
x_mid = projx_fn(x_mid)
|
| 222 |
+
|
| 223 |
+
xt = xt + dt * velocity_fn(x_mid, t0 + half_dt)
|
| 224 |
+
|
| 225 |
+
return projx_fn(xt)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def _rk4_step(
|
| 229 |
+
velocity_model: Callable,
|
| 230 |
+
xt: Tensor,
|
| 231 |
+
t0: Tensor,
|
| 232 |
+
dt: Tensor,
|
| 233 |
+
manifold: Manifold,
|
| 234 |
+
projx: bool = True,
|
| 235 |
+
proju: bool = True,
|
| 236 |
+
) -> Tensor:
|
| 237 |
+
r"""Perform an RK4 step on a manifold.
|
| 238 |
+
|
| 239 |
+
Args:
|
| 240 |
+
velocity_model (Callable): the velocity model
|
| 241 |
+
xt (Tensor): tensor containing the state at time t0
|
| 242 |
+
t0 (Tensor): the time at which this step is taken
|
| 243 |
+
dt (Tensor): the step size
|
| 244 |
+
manifold (Manifold): a manifold object
|
| 245 |
+
projx (bool, optional): whether to project the state onto the manifold. Defaults to True.
|
| 246 |
+
proju (bool, optional): whether to project the velocity onto the tangent plane. Defaults to True.
|
| 247 |
+
|
| 248 |
+
Returns:
|
| 249 |
+
Tensor: tensor containing the state after the step
|
| 250 |
+
"""
|
| 251 |
+
velocity_fn = lambda x, t: (
|
| 252 |
+
manifold.proju(x, velocity_model(x, t)) if proju else velocity_model(x, t)
|
| 253 |
+
)
|
| 254 |
+
projx_fn = lambda x: manifold.projx(x) if projx else x
|
| 255 |
+
|
| 256 |
+
k1 = velocity_fn(xt, t0)
|
| 257 |
+
k2 = velocity_fn(projx_fn(xt + dt * k1 / 3), t0 + dt / 3)
|
| 258 |
+
k3 = velocity_fn(projx_fn(xt + dt * (k2 - k1 / 3)), t0 + dt * 2 / 3)
|
| 259 |
+
k4 = velocity_fn(projx_fn(xt + dt * (k1 - k2 + k3)), t0 + dt)
|
| 260 |
+
|
| 261 |
+
return projx_fn(xt + (k1 + 3 * (k2 + k3) + k4) * dt * 0.125)
|
flow_matching/solver/solver.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
from abc import ABC, abstractmethod
|
| 8 |
+
|
| 9 |
+
from torch import nn, Tensor
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Solver(ABC, nn.Module):
|
| 13 |
+
"""Abstract base class for solvers."""
|
| 14 |
+
|
| 15 |
+
@abstractmethod
|
| 16 |
+
def sample(self, x_0: Tensor = None) -> Tensor:
|
| 17 |
+
...
|
flow_matching/solver/utils.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def get_nearest_times(time_grid: Tensor, t_discretization: Tensor) -> Tensor:
|
| 12 |
+
distances = torch.cdist(
|
| 13 |
+
time_grid.unsqueeze(1),
|
| 14 |
+
t_discretization.unsqueeze(1),
|
| 15 |
+
compute_mode="donot_use_mm_for_euclid_dist",
|
| 16 |
+
)
|
| 17 |
+
nearest_indices = distances.argmin(dim=1)
|
| 18 |
+
|
| 19 |
+
return t_discretization[nearest_indices]
|
flow_matching/utils/.DS_Store
ADDED
|
Binary file (8.2 kB). View file
|
|
|
flow_matching/utils/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
from .categorical_sampler import categorical
|
| 8 |
+
from .model_wrapper import ModelWrapper
|
| 9 |
+
from .utils import expand_tensor_like, gradient, unsqueeze_to_match
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
"unsqueeze_to_match",
|
| 13 |
+
"expand_tensor_like",
|
| 14 |
+
"gradient",
|
| 15 |
+
"categorical",
|
| 16 |
+
"ModelWrapper",
|
| 17 |
+
]
|
flow_matching/utils/categorical_sampler.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def categorical(probs: Tensor) -> Tensor:
|
| 12 |
+
r"""Categorical sampler according to weights in the last dimension of ``probs`` using :func:`torch.multinomial`.
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
probs (Tensor): probabilities.
|
| 16 |
+
|
| 17 |
+
Returns:
|
| 18 |
+
Tensor: Samples.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
return torch.multinomial(probs.flatten(0, -2), 1, replacement=True).view(
|
| 22 |
+
*probs.shape[:-1]
|
| 23 |
+
)
|
flow_matching/utils/manifolds/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
flow_matching/utils/manifolds/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
from .manifold import Euclidean, Manifold
|
| 8 |
+
from .sphere import Sphere
|
| 9 |
+
from .torus import FlatTorus
|
| 10 |
+
from .utils import geodesic
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
"Euclidean",
|
| 14 |
+
"Manifold",
|
| 15 |
+
"Sphere",
|
| 16 |
+
"FlatTorus",
|
| 17 |
+
"geodesic",
|
| 18 |
+
]
|
flow_matching/utils/manifolds/manifold.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the CC-by-NC license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
import abc
|
| 8 |
+
|
| 9 |
+
import torch.nn as nn
|
| 10 |
+
from torch import Tensor
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class Manifold(nn.Module, metaclass=abc.ABCMeta):
|
| 14 |
+
"""A manifold class that contains projection operations and logarithm and exponential maps."""
|
| 15 |
+
|
| 16 |
+
@abc.abstractmethod
|
| 17 |
+
def expmap(self, x: Tensor, u: Tensor) -> Tensor:
|
| 18 |
+
r"""Computes exponential map :math:`\exp_x(u)`.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
x (Tensor): point on the manifold
|
| 22 |
+
u (Tensor): tangent vector at point :math:`x`
|
| 23 |
+
|
| 24 |
+
Raises:
|
| 25 |
+
NotImplementedError: if not implemented
|
| 26 |
+
|
| 27 |
+
Returns:
|
| 28 |
+
Tensor: transported point
|
| 29 |
+
"""
|
| 30 |
+
raise NotImplementedError
|
| 31 |
+
|
| 32 |
+
@abc.abstractmethod
|
| 33 |
+
def logmap(self, x: Tensor, y: Tensor) -> Tensor:
|
| 34 |
+
r"""Computes logarithmic map :math:`\log_x(y)`.
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
x (Tensor): point on the manifold
|
| 38 |
+
y (Tensor): point on the manifold
|
| 39 |
+
|
| 40 |
+
Raises:
|
| 41 |
+
NotImplementedError: if not implemented
|
| 42 |
+
|
| 43 |
+
Returns:
|
| 44 |
+
Tensor: tangent vector at point :math:`x`
|
| 45 |
+
"""
|
| 46 |
+
raise NotImplementedError
|
| 47 |
+
|
| 48 |
+
@abc.abstractmethod
|
| 49 |
+
def projx(self, x: Tensor) -> Tensor:
|
| 50 |
+
"""Project point :math:`x` on the manifold.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
x (Tensor): point to be projected
|
| 54 |
+
|
| 55 |
+
Raises:
|
| 56 |
+
NotImplementedError: if not implemented
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
Tensor: projected point on the manifold
|
| 60 |
+
"""
|
| 61 |
+
raise NotImplementedError
|
| 62 |
+
|
| 63 |
+
@abc.abstractmethod
|
| 64 |
+
def proju(self, x: Tensor, u: Tensor) -> Tensor:
|
| 65 |
+
"""Project vector :math:`u` on a tangent space for :math:`x`.
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
x (Tensor): point on the manifold
|
| 69 |
+
u (Tensor): vector to be projected
|
| 70 |
+
|
| 71 |
+
Raises:
|
| 72 |
+
NotImplementedError: if not implemented
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
Tensor: projected tangent vector
|
| 76 |
+
"""
|
| 77 |
+
raise NotImplementedError
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class Euclidean(Manifold):
|
| 81 |
+
"""The Euclidean manifold."""
|
| 82 |
+
|
| 83 |
+
def expmap(self, x: Tensor, u: Tensor) -> Tensor:
|
| 84 |
+
return x + u
|
| 85 |
+
|
| 86 |
+
def logmap(self, x: Tensor, y: Tensor) -> Tensor:
|
| 87 |
+
return y - x
|
| 88 |
+
|
| 89 |
+
def projx(self, x: Tensor) -> Tensor:
|
| 90 |
+
return x
|
| 91 |
+
|
| 92 |
+
def proju(self, x: Tensor, u: Tensor) -> Tensor:
|
| 93 |
+
return u
|