cnedwards commited on
Commit
18dcaa0
·
verified ·
1 Parent(s): 013d1f8

Update README.md

Browse files
Files changed (1) hide show
  1. README.md +44 -88
README.md CHANGED
@@ -12,22 +12,55 @@ pipeline_tag: other
12
 
13
  # AssayFormer
14
 
 
 
 
 
 
 
 
 
15
  The amortized gene ranker from **AssayLoop**: *Biology-in-the-loop: Amortized
16
  Adaptive Hit Discovery in CRISPR Screens*. Given a screen description and the
17
- labels revealed so far, it scores every gene in a 24,217-gene vocabulary and
18
- proposes the next batch to assay.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- This is the `+ GRPO` row of the paper's main table — the transformer with BPMF
21
- gene embeddings, supervised on 1,349 screens and then fine-tuned with GRPO.
 
 
 
 
 
 
22
 
23
- - **Code:** https://github.com/genentech/assayloop
24
- - **Benchmark data:** [Genentech/assaybench](https://huggingface.co/datasets/Genentech/assaybench)
25
- - **Benchmark package:** [`assaybench`](https://pypi.org/project/assaybench/) on PyPI
26
 
27
  ## Results
28
 
29
- Twenty held-out genome-wide screens, ten rounds of one hundred genes, against a
30
- 21,147-gene universe (`--min-screen-freq 2`).
31
 
32
  | metric | value |
33
  |---|---|
@@ -38,8 +71,6 @@ Twenty held-out genome-wide screens, ten rounds of one hundred genes, against a
38
  | DepMap common-essential fraction | 0.407 |
39
  | Effective pathways EP-B / EP-S / EP-D | 19.3 / 46.7 / 65.0 |
40
 
41
- EP is the effective number of Reactome level-2 groups, rarefied to 30 / 200 /
42
- 6000 annotated genes; a uniform draw scores 21.8 / 56.4 / 82.1.
43
 
44
  ## Files
45
 
@@ -49,10 +80,7 @@ EP is the effective number of Reactome level-2 groups, rarefied to 30 / 200 /
49
  | `config.json` | architecture and RL hyperparameters |
50
  | `vocab.json` | the 24,217-entry gene vocabulary (index 0 is `<unk>`) |
51
 
52
- `model_last.pt` is the checkpoint the paper reports, not the best-on-validation
53
- one. Held-out EF dips and recovers during GRPO, so the run also kept a
54
- best-on-val snapshot; every number above, every figure, and the handoff
55
- experiments use the final epoch. If you want the gene embedding table on its
56
  own it is `state_dict["gene_emb.weight"]`, shape (24217, 10).
57
 
58
  ## Architecture
@@ -69,85 +97,13 @@ gene embedding.
69
  | context | up to 1024 observations |
70
 
71
  **The description input is a precomputed embedding, not text.** The model
72
- expects a 1536-d vector from OpenAI `text-embedding-3-small`; there is no text
73
- encoder in the checkpoint. The `bert_model` field in `config.json` is inert for
74
- this `encoder_type` — it applies only to the `modernbert_text` variant. The
75
  repository ships the screen-description embeddings for the benchmark screens, so
76
  you only need an embedding backend for screens of your own.
77
 
78
- ## Usage
79
-
80
- ```python
81
- from huggingface_hub import snapshot_download
82
- from assayloop.models.amortized_ranker import AmortizedRankerModel
83
-
84
- ckpt = snapshot_download("Genentech/assayformer")
85
- model = AmortizedRankerModel(checkpoint=ckpt, ckpt_file="model_last.pt")
86
- ```
87
-
88
- Note the explicit `ckpt_file`: the loader defaults to `model.pt`, which is not
89
- in this repository.
90
-
91
- Then run it on a held-out screen and score the trajectory:
92
-
93
- ```python
94
- from assaybench import SequentialLoop, enrichment_factor
95
- from assayloop.acquisitions.greedy_from_model import GreedyFromModel
96
- from assayloop.tasks.gene_batch import AssayBenchGeneBatchTask
97
- from assayloop.tasks.screen_sets import load_screens
98
-
99
- screen = load_screens(target_set="public")[0] # the paper's 20-screen test set
100
- task = AssayBenchGeneBatchTask(screen)
101
- run = SequentialLoop(task, model, GreedyFromModel(), metrics=[], batch_size=100).run(n_steps=10)
102
-
103
- picked = [g for step in run.history for g in step.acquired_batch]
104
- hits = [g for g, h in zip(screen.genes, screen.hits) if h]
105
- print(enrichment_factor(picked, screen.genes, hits, budget=1000)) # 6.96 on U_1733_merged
106
- ```
107
-
108
- No API key is needed: the screen-description embeddings for the benchmark
109
- screens ship with the repository, so `task_context` resolves offline.
110
-
111
- That prints one number for one screen, which is not the 4.83 in the table
112
- above. Two differences: the table is the mean over all twenty screens, and it
113
- scores against the full-genome candidate pool — the union of the twenty
114
- libraries, restricted to genes measured in at least two of them — which the
115
- task takes as `universe_genes=`. Picking from a 21,147-gene pool rather than
116
- one screen's own library is harder, and it is the number the paper reports.
117
- `python -m assayloop.scripts.full_genome_table --min-screen-freq 2` reproduces
118
- the table row for row.
119
-
120
- ## Training
121
-
122
- Supervised on 1,349 screens (hits objective, leave-one-out, 40 epochs), then
123
- GRPO for 100 epochs on the `public_train` split with `public_validation` held
124
- out: group size 8, 10 steps of 100 genes, lr 1e-5, entropy coefficient 0.01, no
125
- KL penalty, context reset every 25 epochs, seed 19. The reward is the
126
- context-delta form; `config.json` has the rest.
127
-
128
- ## Gene embeddings and BPMF
129
-
130
- The gene embedding table was initialised from the posterior mean of a K=10
131
- Bayesian PMF fit and then updated by supervised training and GRPO. Every row
132
- moved, but not far: median cosine to the BPMF initialisation is 0.9999 and the
133
- median relative displacement is 1.6%. Treat the table as approximately the BPMF
134
- gene factors rather than as independently learned representations. The BPMF fit
135
- itself — the screen loadings, the posterior samples, the predicted
136
- probabilities — is not part of this release.
137
-
138
- ## Limitations
139
-
140
- - Trained on BioGRID ORCS screens; the split is by year, and the vocabulary is
141
- fixed at training time. Genes outside it map to `<unk>`.
142
- - EF is computed against each screen's own library. A gene the screen never
143
- assayed can never be a hit, and 10% of this model's picks fall there.
144
- - 40.7% of its picks are DepMap common-essential — a real fraction of the hit
145
- rate comes from genes that are essential in most cell lines.
146
 
147
  ## Citation
148
 
149
- The arXiv identifier is not assigned yet; this entry will be updated when it is.
150
-
151
  ```bibtex
152
  @article{assayloop2026,
153
  title = {Biology-in-the-loop: Amortized Adaptive Hit Discovery in CRISPR Screens},
 
12
 
13
  # AssayFormer
14
 
15
+
16
+ [Website](https://genentech.github.io/AssayLoop) | [Models and Data](https://huggingface.co/collections/Genentech/assaybench) | [Paper](README_DETAILED.md) | [![Benchmark Package](https://img.shields.io/pypi/v/assaybench)](https://pypi.org/project/assaybench/)
17
+
18
+ ![The task: a screen is a library of genes, a phenotype and a hit set. Each round, a method
19
+ sees the phenotype and everything it has already assayed, and chooses the next hundred
20
+ genes.](docs/assets/figures/figure1.png)
21
+
22
+
23
  The amortized gene ranker from **AssayLoop**: *Biology-in-the-loop: Amortized
24
  Adaptive Hit Discovery in CRISPR Screens*. Given a screen description and the
25
+ hit labels revealed so far, it scores every gene in a 24,217-gene vocabulary and
26
+ proposes the next batch to assay. In the paper, a 21,147 gene subset was used.
27
+
28
+
29
+ ## Usage
30
+
31
+ ```python
32
+ from huggingface_hub import snapshot_download
33
+ from assayloop.models.amortized_ranker import AmortizedRankerModel
34
+
35
+ ckpt = snapshot_download("Genentech/assayformer")
36
+ model = AmortizedRankerModel(checkpoint=ckpt, ckpt_file="model_last.pt")
37
+ ```
38
+
39
+ Note the explicit `ckpt_file`: the loader defaults to `model.pt`, which is not
40
+ in this repository.
41
+
42
+ Then run it on a held-out screen and score the trajectory:
43
+
44
+ ```python
45
+ from assaybench import SequentialLoop, enrichment_factor
46
+ from assayloop.acquisitions.greedy_from_model import GreedyFromModel
47
+ from assayloop.tasks import AssayBenchGeneBatchTask, gene_universe, load_screens
48
+
49
+ screens = load_screens(target_set="public") # the paper's 20-screen test set
50
+ universe = gene_universe(screens) # the f2 pool, 21,147 genes
51
 
52
+ screen = screens[0]
53
+ task = AssayBenchGeneBatchTask(screen, universe_genes=universe)
54
+ run = SequentialLoop(task, model, GreedyFromModel(), metrics=[], batch_size=100).run(n_steps=10)
55
+
56
+ picked = [g for step in run.history for g in step.acquired_batch]
57
+ hits = [g for g, h in zip(screen.genes, screen.hits) if h]
58
+ print(enrichment_factor(picked, screen.genes, hits, universe=universe, budget=1000)) # 7.61 on U_1733_merged
59
+ ```
60
 
 
 
 
61
 
62
  ## Results
63
 
 
 
64
 
65
  | metric | value |
66
  |---|---|
 
71
  | DepMap common-essential fraction | 0.407 |
72
  | Effective pathways EP-B / EP-S / EP-D | 19.3 / 46.7 / 65.0 |
73
 
 
 
74
 
75
  ## Files
76
 
 
80
  | `config.json` | architecture and RL hyperparameters |
81
  | `vocab.json` | the 24,217-entry gene vocabulary (index 0 is `<unk>`) |
82
 
83
+ `model_last.pt` is the checkpoint the paper reports. If you want the gene embedding table on its
 
 
 
84
  own it is `state_dict["gene_emb.weight"]`, shape (24217, 10).
85
 
86
  ## Architecture
 
97
  | context | up to 1024 observations |
98
 
99
  **The description input is a precomputed embedding, not text.** The model
100
+ expects a 1536-d vector from OpenAI `text-embedding-3-small`. The
 
 
101
  repository ships the screen-description embeddings for the benchmark screens, so
102
  you only need an embedding backend for screens of your own.
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  ## Citation
106
 
 
 
107
  ```bibtex
108
  @article{assayloop2026,
109
  title = {Biology-in-the-loop: Amortized Adaptive Hit Discovery in CRISPR Screens},