clipnet 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clipnet-0.2.1/LICENSE +21 -0
- clipnet-0.2.1/PKG-INFO +23 -0
- clipnet-0.2.1/README.md +166 -0
- clipnet-0.2.1/clipnet/__init__.py +9 -0
- clipnet-0.2.1/clipnet/_calculate_activator_interaction.py +159 -0
- clipnet-0.2.1/clipnet/_calculate_dataset_params.py +123 -0
- clipnet-0.2.1/clipnet/_calculate_ism_shuffle.py +157 -0
- clipnet-0.2.1/clipnet/_calculate_performance_metrics.py +150 -0
- clipnet-0.2.1/clipnet/_fit_nn.py +63 -0
- clipnet-0.2.1/clipnet/_get_activation_maps.py +63 -0
- clipnet-0.2.1/clipnet/_get_filter_gc_content.py +64 -0
- clipnet-0.2.1/clipnet/_rnn_v10_2k.py +85 -0
- clipnet-0.2.1/clipnet/_rnn_v10_exp.py +94 -0
- clipnet-0.2.1/clipnet/_rnn_v11.py +91 -0
- clipnet-0.2.1/clipnet/attribute.py +209 -0
- clipnet-0.2.1/clipnet/cgen.py +162 -0
- clipnet-0.2.1/clipnet/cli.py +431 -0
- clipnet-0.2.1/clipnet/clipnet.py +473 -0
- clipnet-0.2.1/clipnet/custom_loss.py +55 -0
- clipnet-0.2.1/clipnet/epistasis.py +142 -0
- clipnet-0.2.1/clipnet/ism_shuffle.py +74 -0
- clipnet-0.2.1/clipnet/rnn_v10.py +96 -0
- clipnet-0.2.1/clipnet/shuffle.py +128 -0
- clipnet-0.2.1/clipnet/utils.py +334 -0
- clipnet-0.2.1/clipnet/visualize.py +333 -0
- clipnet-0.2.1/clipnet.egg-info/PKG-INFO +23 -0
- clipnet-0.2.1/clipnet.egg-info/SOURCES.txt +31 -0
- clipnet-0.2.1/clipnet.egg-info/dependency_links.txt +1 -0
- clipnet-0.2.1/clipnet.egg-info/entry_points.txt +2 -0
- clipnet-0.2.1/clipnet.egg-info/requires.txt +13 -0
- clipnet-0.2.1/clipnet.egg-info/top_level.txt +1 -0
- clipnet-0.2.1/pyproject.toml +45 -0
- clipnet-0.2.1/setup.cfg +4 -0
clipnet-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adam Youlin He
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
clipnet-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clipnet
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: CLIPNET is an ensembled convolutional neural network for predicting transcription initiation from DNA sequence at single nucleotide resolution.
|
|
5
|
+
Author-email: Adam He <adamyhe@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: homepage, https://github.com/adamyhe/clipnet
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: gputil
|
|
11
|
+
Requires-Dist: h5py
|
|
12
|
+
Requires-Dist: joblib>=1.3
|
|
13
|
+
Requires-Dist: matplotlib
|
|
14
|
+
Requires-Dist: numpy<2.0.0,>=1.26.3
|
|
15
|
+
Requires-Dist: pandas
|
|
16
|
+
Requires-Dist: pyfastx>=1.1
|
|
17
|
+
Requires-Dist: scipy
|
|
18
|
+
Requires-Dist: seqlogo
|
|
19
|
+
Requires-Dist: shap==0.44.1
|
|
20
|
+
Requires-Dist: tensorflow[and-cuda]<2.15.0,>=2.14.0
|
|
21
|
+
Requires-Dist: silence_tensorflow
|
|
22
|
+
Requires-Dist: tqdm>=4.64.1
|
|
23
|
+
Dynamic: license-file
|
clipnet-0.2.1/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# CLIPNET
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/clipnet/)
|
|
4
|
+
[](https://pepy.tech/projects/clipnet)
|
|
5
|
+
[](https://doi.org/10.5281/zenodo.10408623)
|
|
6
|
+
|
|
7
|
+
CLIPNET (Convolutionally Learned, Initiation-Predicting NETwork) is an ensembled convolutional neural network that predicts transcription initiation from DNA sequence at single nucleotide resolution. We describe CLIPNET in our [preprint](https://www.biorxiv.org/content/10.1101/2024.03.13.583868) on bioRxiv. This repository contains code for working with CLIPNET, namely for generating predictions and feature interpretations and performing *in silico* mutagenesis scans. To reproduce the figures in our paper, please see the [clipnet_paper GitHub repo](https://github.com/Danko-Lab/clipnet_paper/).
|
|
8
|
+
|
|
9
|
+
## PyTorch reimplementation and port
|
|
10
|
+
|
|
11
|
+
Code to port the TensorFlow models to PyTorch is available as part of [PersonalBPNet](https://github.com/adamyhe/PersonalBPNet/), which also includes a from-scratch reimplementation of CLIPNET in PyTorch with a context length of 2114 bp. The below instructions are for working with CLIPNET in TensorFlow.
|
|
12
|
+
|
|
13
|
+
## CODE REFACTORING NOTICE
|
|
14
|
+
|
|
15
|
+
I have significantly altered the structure of this code base since its original release with the preprint. The new CLIPNET package should be significantly easier to use (`pip` installable, with clearer CLI and API). To access the code as it was prior to this refactoring, please check out the (unmaintained) [`deprecated`](https://github.com/Danko-Lab/clipnet/tree/deprecated) branch.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
To install CLIPNET, we recommend creating an isolated environment. For example, with conda/mamba:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
mamba create -n clipnet -c conda-forge python~=3.9
|
|
23
|
+
mamba activate clipnet
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then install with pip:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# From PyPI
|
|
30
|
+
pip install clipnet
|
|
31
|
+
# Or from source:
|
|
32
|
+
pip install git+https://github.com/Danko-Lab/clipnet.git
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
You may need to configure your CUDA/cudatoolkit/cudnn paths to get GPU support working. See the [tensorflow documentation](https://www.tensorflow.org/install/gpu) for more information.
|
|
36
|
+
|
|
37
|
+
## Download models
|
|
38
|
+
|
|
39
|
+
Pretrained CLIPNET models are available on [Zenodo](https://zenodo.org/doi/10.5281/zenodo.10408622).
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
mkdir -p clipnet_models/
|
|
43
|
+
for fold in {1..9};
|
|
44
|
+
do wget https://zenodo.org/records/10408623/files/fold_${fold}.h5 -P clipnet_models/;
|
|
45
|
+
done
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Alternatively, they can be accessed via [HuggingFace](https://huggingface.co/adamyhe/clipnet).
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### Input data
|
|
53
|
+
|
|
54
|
+
CLIPNET was trained on a [population-scale PRO-cap dataset](http://dx.doi.org/10.1038/s41467-020-19829-z) derived from human lymphoblastoid cell lines, matched with individualized genome sequences (1kGP). CLIPNET accepts 1000 bp sequences as input and imputes PRO-cap coverage (RPM) in the center 500 bp.
|
|
55
|
+
|
|
56
|
+
CLIPNET can either work on haploid reference sequences (e.g. hg38) or on individualized sequences (e.g. 1kGP). When constructing individualized sequences, we made two major simplifications: (1) We considered only SNPs and (2) we used unphased SNP genotypes.
|
|
57
|
+
|
|
58
|
+
We encode sequences using a "two-hot" encoding. That is, we encoded each individual nucleotide at a given position using a one-hot encoding scheme, then represented the unphased diploid sequence as the sum of the two one-hot encoded nucleotides at each position. The sequence "AYCR" (= A(C/T)C(A/G)), for example, would be encoded as: `[[2, 0, 0, 0], [0, 1, 0, 1], [0, 2, 0, 0], [1, 0, 1, 0]]`.
|
|
59
|
+
|
|
60
|
+
### Colab examples
|
|
61
|
+
|
|
62
|
+
Google Colab examples for analyzing and applying CLIPNET are available through the following links:
|
|
63
|
+
|
|
64
|
+
- [Basic tutorial](https://colab.research.google.com/drive/1ojhoKC5IjHjjxltZdAktSkuNFU0Wvjce?usp=sharing) illustrating how to generate predictions and attributions with the models.
|
|
65
|
+
- Variant effect prediction and interpretation (TODO)
|
|
66
|
+
- MPRA optimization/design (TODO)
|
|
67
|
+
|
|
68
|
+
### Command line interface
|
|
69
|
+
|
|
70
|
+
CLIPNET can be accessed via a CLI:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
clipnet -h
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### Predictions
|
|
77
|
+
|
|
78
|
+
The `predict` command can be used to generate predictions:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
clipnet predict -f data/test.fa -o data/test_predictions.npz -m clipnet_models/ -v
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The `-m` flag should be used to specify either a path to the directory containing the CLIPNET models (in which case the averaged predictions across all model replicates will be returned) or a specific model path (in which case only the predictions of that model will be returned).
|
|
85
|
+
|
|
86
|
+
To input individualized sequences, heterozygous positions should be represented using the IUPAC ambiguity codes R (A/G), Y (C/T), S (C/G), W (A/T), K (G/T), M (A/C).
|
|
87
|
+
|
|
88
|
+
The output npz file will contain two arrays. The first output (`"arr_0"`, "profile") is a length 1000 vector (500 plus strand concatenated with 500 minus strand) representing the predicted base-resolution profile/shape of initiation. The second output (`"arr_0"`, "quantity") represents the total PRO-cap quantity on both strands.
|
|
89
|
+
|
|
90
|
+
To generate actual predicted tracks, the profile prediction should be rescaled by the quantity prediction. For example:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import numpy as np
|
|
94
|
+
|
|
95
|
+
f = np.load("data/test_predictions.npz")
|
|
96
|
+
profile = f["arr_0"]
|
|
97
|
+
quantity = f["arr_1"]
|
|
98
|
+
profile_scaled = (profile / np.sum(profile, axis=1)[:, None]) * quantity
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Attributions
|
|
102
|
+
|
|
103
|
+
CLIPNET uses [DeepSHAP](https://shap.readthedocs.io/en/latest/generated/shap.DeepExplainer.html) to generate attributions. To generate DeepSHAP scores, use the `attribute` command. This script takes a fasta file containing 1000 bp records and outputs DeepSHAP attributions and optionally one-hot encoded sequences. Please note that both attribution and ohe are saved as length last for compatibility with [tfmodisco-lite](https://github.com/jmschrei/tfmodisco-lite/).
|
|
104
|
+
|
|
105
|
+
Two different attribution modes that can be set with `-a/--attribution_type`: `profile` and `quantity`. The `profile` mode calculates interpretations for the profile node of the model (using the profile metric proposed in BPNet), while the `quantity` mode calculates interpretations for the quantity node of the model.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
clipnet attribute \
|
|
109
|
+
-f data/test.fa.gz \
|
|
110
|
+
-o data/test_quantity_shap.npz \
|
|
111
|
+
-m clipnet_models/ \
|
|
112
|
+
-a quantity \
|
|
113
|
+
-v
|
|
114
|
+
|
|
115
|
+
# -c maybe needed to avoid precision errors.
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Note that while CLIPNET accepts two-hot encoded sequences to accomodate heterozygous positions, attributions are much more interpretable when using a haploid/fully homozygous genome, so we recommend avoiding heterozygous positions for attributions. Also note that these are actual contribution scores, as opposed to hypothetical contribution scores. Specifically, non-reference nucleotides are set to zero. To return attribution scores for all nucleotides, use the `-y` flag.
|
|
119
|
+
|
|
120
|
+
#### Discovering epistatic motifs
|
|
121
|
+
|
|
122
|
+
`clipnet` supports epistasis analyses using [Deep Feature Interaction Maps (DFIM)](https://github.com/kundajelab/dfim). Please note that this is a custom reimplementation of DFIM using DeepSHAP as the attribution backend, as the original DFIM package is unmaintained and difficult to install. DFIM scores can be calculated for a given fasta file using the `epistasis` command:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
clipnet epistasis \
|
|
126
|
+
-f data/test.fa \
|
|
127
|
+
-o data/test_dfim_profile.npz \
|
|
128
|
+
-m clipnet_models/ \
|
|
129
|
+
-s 250 -e 750 \
|
|
130
|
+
-a profile \
|
|
131
|
+
-v
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Please note DFIM scores don't properly account for things like global epistasis/nonlinearity, which can cause misleading interpretations. For a more robust (but time-consuming) method for estimating interaction effects, see [SQUID](https://github.com/evanseitz/squid-nn).
|
|
135
|
+
|
|
136
|
+
#### Genomic *in silico* mutagenesis scans
|
|
137
|
+
|
|
138
|
+
To generate genomic *in silico* mutagenesis scans, use the `ism_shuffle` script. This script takes a fasta file containing 1000 bp records and outputs an npz file containing the ISM shuffle results (`corr_ism_shuffle` and `logfc_ism_shuffle`) for each record. For example:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
clipnet ism_shuffle -f data/test.fa -o data/test_ism.npz -m clipnet_models/ -v
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### API usage
|
|
145
|
+
|
|
146
|
+
CLIPNET models can be directly loaded as follows. Individual models can simply be loaded using tensorflow:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import tensorflow as tf
|
|
150
|
+
|
|
151
|
+
nn = tf.keras.models.load_model("clipnet_models/fold_1.h5", compile=False)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The model ensemble is constructed by averaging track and quantity outputs across all 9 model folds. To make this easy, we've provided a simple API in the `clipnet.clipnet.CLIPNET` class for doing this. Moreover, to make reading fasta files into the correct format easier, we've provided the helper function `clipnet.utils.get_twohot_fasta_sequences`. For example:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
import sys
|
|
158
|
+
from clipnet.clipnet import CLIPNET
|
|
159
|
+
from clipnet.utils import get_twohot_fasta_sequences
|
|
160
|
+
|
|
161
|
+
nn = CLIPNET()
|
|
162
|
+
ensemble = nn.construct_ensemble("clipnet_models/")
|
|
163
|
+
seqs = get_twohot_fasta_sequences("data/test.fa")
|
|
164
|
+
|
|
165
|
+
predictions = ensemble.predict(seqs)
|
|
166
|
+
```
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
## NOT IMPLEMENTED
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Calculate contribution scores using shap.DeepExplainer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import gc
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import shap
|
|
12
|
+
import tensorflow as tf
|
|
13
|
+
|
|
14
|
+
import .utils
|
|
15
|
+
|
|
16
|
+
# This will fix an error message for running tf.__version__==2.5
|
|
17
|
+
shap.explainers._deep.deep_tf.op_handlers["AddV2"] = (
|
|
18
|
+
shap.explainers._deep.deep_tf.passthrough
|
|
19
|
+
)
|
|
20
|
+
tf.compat.v1.disable_v2_behavior()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
25
|
+
parser.add_argument("core_promoter_seq", type=str, help="")
|
|
26
|
+
parser.add_argument("motif", type=str, help="Where to write DeepSHAP scores.")
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--model_dir",
|
|
29
|
+
type=str,
|
|
30
|
+
default="ensemble_models",
|
|
31
|
+
help="Directory to load models from",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--mode",
|
|
35
|
+
type=str,
|
|
36
|
+
default="quantity",
|
|
37
|
+
help="Calculate interaction strength using quantity or DFIM",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument("--gpu", action="store_true", help="Enable GPU.")
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--use_specific_gpu",
|
|
42
|
+
type=int,
|
|
43
|
+
default=0,
|
|
44
|
+
help="Index of GPU to use (starting from 0). Does nothing if --gpu is not set.",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--n_subset",
|
|
48
|
+
type=int,
|
|
49
|
+
default=100,
|
|
50
|
+
help="Maximum number of sequences to use as background. \
|
|
51
|
+
Default is 100 to ensure reasonably fast compute on large datasets.",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--seed",
|
|
55
|
+
type=int,
|
|
56
|
+
default=617,
|
|
57
|
+
help="Random seed for selecting background sequences.",
|
|
58
|
+
)
|
|
59
|
+
args = parser.parse_args()
|
|
60
|
+
np.random.seed(args.seed)
|
|
61
|
+
|
|
62
|
+
assert args.mode in [
|
|
63
|
+
"quantity",
|
|
64
|
+
"dfim",
|
|
65
|
+
], "mode must be either quantity or dfim."
|
|
66
|
+
|
|
67
|
+
# Load sequences ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
68
|
+
|
|
69
|
+
concat_ref = "".join(shuffled_ref)
|
|
70
|
+
embedded_promoter = (
|
|
71
|
+
ushuffle.shuffle(
|
|
72
|
+
concat_ref, int(np.floor((1000 - len(core_promoter_seq)) / 2)), 2
|
|
73
|
+
)
|
|
74
|
+
+ core_promoter_seq
|
|
75
|
+
+ ushuffle.shuffle(
|
|
76
|
+
concat_ref, int(np.ceil((1000 - len(core_promoter_seq)) / 2)), 2
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
seqs_to_explain = np.array([utils.OneHotDNA(seq).onehot for seq in sequences])
|
|
80
|
+
|
|
81
|
+
# Perform dinucleotide shuffle on n_subset random sequences
|
|
82
|
+
if len(sequences) < args.n_subset:
|
|
83
|
+
args.n_subset = len(sequences)
|
|
84
|
+
reference = [
|
|
85
|
+
sequences[i]
|
|
86
|
+
for i in np.random.choice(
|
|
87
|
+
np.array(range(len(sequences))), size=args.n_subset, replace=False
|
|
88
|
+
)
|
|
89
|
+
]
|
|
90
|
+
shuffled_reference = [
|
|
91
|
+
utils.kshuffle(rec.seq, random_seed=args.seed)[0] for rec in reference
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# One-hot encode shuffled sequences
|
|
95
|
+
onehot_reference = np.array(
|
|
96
|
+
[utils.OneHotDNA(seq).onehot for seq in shuffled_reference]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Load model and create explainer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
100
|
+
|
|
101
|
+
if args.gpu:
|
|
102
|
+
gpus = tf.config.experimental.list_physical_devices("GPU")
|
|
103
|
+
for gpu in gpus:
|
|
104
|
+
tf.config.experimental.set_memory_growth(gpu, True)
|
|
105
|
+
tf.config.set_visible_devices(gpus[args.use_specific_gpu], "GPU")
|
|
106
|
+
else:
|
|
107
|
+
tf.config.set_visible_devices([], "GPU")
|
|
108
|
+
|
|
109
|
+
if args.model_fp is None:
|
|
110
|
+
models = [
|
|
111
|
+
tf.keras.models.load_model(f"{args.model_dir}/fold_{i}.h5", compile=False)
|
|
112
|
+
for i in range(1, 10)
|
|
113
|
+
]
|
|
114
|
+
else:
|
|
115
|
+
models = [tf.keras.models.load_model(args.model_fp, compile=False)]
|
|
116
|
+
if args.mode == "quantity":
|
|
117
|
+
contrib = [model.output[1] for model in models]
|
|
118
|
+
else:
|
|
119
|
+
contrib = [
|
|
120
|
+
tf.reduce_mean(
|
|
121
|
+
tf.stop_gradient(tf.nn.softmax(model.output[0], axis=-1))
|
|
122
|
+
* model.output[0],
|
|
123
|
+
axis=-1,
|
|
124
|
+
keepdims=True,
|
|
125
|
+
)
|
|
126
|
+
for model in models
|
|
127
|
+
]
|
|
128
|
+
explainers = [
|
|
129
|
+
shap.DeepExplainer((model.input, contrib), onehot_reference)
|
|
130
|
+
for (model, contrib) in zip(models, contrib)
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
# Calculate scores ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
134
|
+
|
|
135
|
+
raw_explanations = {i: [] for i in range(len(explainers))}
|
|
136
|
+
batch_size = 256
|
|
137
|
+
for i in range(len(explainers)):
|
|
138
|
+
for j in range(0, len(seqs_to_explain), batch_size):
|
|
139
|
+
shap_values = explainers[i].shap_values(seqs_to_explain[j : j + batch_size])
|
|
140
|
+
raw_explanations[i].append(shap_values)
|
|
141
|
+
gc.collect()
|
|
142
|
+
|
|
143
|
+
concat_explanations = []
|
|
144
|
+
for k in raw_explanations.keys():
|
|
145
|
+
concat_explanations.append(
|
|
146
|
+
np.concatenate([exp for exp in raw_explanations[k]], axis=1).sum(axis=0)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
mean_explanations = np.array(concat_explanations).mean(axis=0)
|
|
150
|
+
scaled_explanations = mean_explanations * seqs_to_explain
|
|
151
|
+
|
|
152
|
+
# Save scores ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
153
|
+
|
|
154
|
+
np.savez_compressed(args.score_fp, scaled_explanations.swapaxes(1, 2))
|
|
155
|
+
np.savez_compressed(args.seq_fp, (seqs_to_explain / 2).astype(int).swapaxes(1, 2))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
main()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
## NOT IMPLEMENTED. PART OF THE FITTING SCRIPTS
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Calculates dataset parameters needed by clipnet. Supply a path to the processed data
|
|
5
|
+
and an output directory. This script will write json files to the output directory
|
|
6
|
+
for use in model training.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def write_dataset_params(i, datadir, outdir):
|
|
17
|
+
outdir = f"{outdir}/f{i}/"
|
|
18
|
+
os.makedirs(outdir, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
test_folds = [i]
|
|
21
|
+
val_folds = [i % 9 + 1]
|
|
22
|
+
train_folds = [j for j in range(1, 10) if j not in test_folds + val_folds]
|
|
23
|
+
print(train_folds, val_folds, test_folds)
|
|
24
|
+
|
|
25
|
+
dataset_params = {
|
|
26
|
+
"train_seq": [
|
|
27
|
+
os.path.join(datadir, f"concat_sequence_{fold}.npz") for fold in train_folds
|
|
28
|
+
],
|
|
29
|
+
"train_procap": [
|
|
30
|
+
os.path.join(datadir, f"concat_procap_{fold}.npz") for fold in train_folds
|
|
31
|
+
],
|
|
32
|
+
"val_seq": [
|
|
33
|
+
os.path.join(datadir, f"concat_sequence_{fold}.npz") for fold in val_folds
|
|
34
|
+
],
|
|
35
|
+
"val_procap": [
|
|
36
|
+
os.path.join(datadir, f"concat_procap_{fold}.npz") for fold in val_folds
|
|
37
|
+
],
|
|
38
|
+
"test_seq": [
|
|
39
|
+
os.path.join(datadir, f"concat_sequence_{fold}.npz") for fold in test_folds
|
|
40
|
+
],
|
|
41
|
+
"test_procap": [
|
|
42
|
+
os.path.join(datadir, f"concat_procap_{fold}.npz") for fold in test_folds
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
dataset_params["n_train_folds"] = len(train_folds)
|
|
47
|
+
dataset_params["n_val_folds"] = len(val_folds)
|
|
48
|
+
dataset_params["n_test_folds"] = len(test_folds)
|
|
49
|
+
|
|
50
|
+
# Calculate n_samples_per_chunk
|
|
51
|
+
dataset_params["n_samples_per_train_fold"] = [
|
|
52
|
+
np.load(f)["arr_0"].shape[0] for f in dataset_params["train_procap"]
|
|
53
|
+
]
|
|
54
|
+
dataset_params["n_samples_per_val_fold"] = [
|
|
55
|
+
np.load(f)["arr_0"].shape[0] for f in dataset_params["val_procap"]
|
|
56
|
+
]
|
|
57
|
+
dataset_params["n_samples_per_test_fold"] = [
|
|
58
|
+
np.load(f)["arr_0"].shape[0] for f in dataset_params["test_procap"]
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
dataset_params["window_length"] = np.load(dataset_params["train_seq"][0])["arr_0"][
|
|
62
|
+
0
|
|
63
|
+
].shape[0]
|
|
64
|
+
|
|
65
|
+
dataset_params["pad"] = int(dataset_params["window_length"] / 4)
|
|
66
|
+
dataset_params["output_length"] = int(
|
|
67
|
+
2 * (dataset_params["window_length"] - 2 * dataset_params["pad"])
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
dataset_params["weight"] = 1 / 500
|
|
71
|
+
|
|
72
|
+
output_fp = os.path.join(outdir, "dataset_params.json")
|
|
73
|
+
|
|
74
|
+
with open(output_fp, "w") as handle:
|
|
75
|
+
json.dump(dataset_params, handle, indent=4, sort_keys=True)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def main():
|
|
79
|
+
parser = argparse.ArgumentParser()
|
|
80
|
+
parser.add_argument("datadir", type=str, help="directory containing data")
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"outdir",
|
|
83
|
+
type=str,
|
|
84
|
+
help="directory to save dataset params to (where models will be saved)",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--threads",
|
|
88
|
+
type=int,
|
|
89
|
+
default=9,
|
|
90
|
+
help="number of threads to use. Only used if not --fold is not selected.",
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--fold",
|
|
94
|
+
type=int,
|
|
95
|
+
default=None,
|
|
96
|
+
help="fold to calculate dataset params for (will only run one).",
|
|
97
|
+
)
|
|
98
|
+
args = parser.parse_args()
|
|
99
|
+
if args.threads <= 0:
|
|
100
|
+
raise ValueError("--threads must be a positive integer")
|
|
101
|
+
if args.fold is not None:
|
|
102
|
+
write_dataset_params(args.fold, args.datadir, args.outdir)
|
|
103
|
+
else:
|
|
104
|
+
if args.threads == 1:
|
|
105
|
+
for i in range(1, 10):
|
|
106
|
+
write_dataset_params(i, args.datadir, args.outdir)
|
|
107
|
+
elif args.threads > 1:
|
|
108
|
+
import itertools
|
|
109
|
+
import multiprocessing as mp
|
|
110
|
+
|
|
111
|
+
with mp.Pool(9) as p:
|
|
112
|
+
p.starmap(
|
|
113
|
+
write_dataset_params,
|
|
114
|
+
zip(
|
|
115
|
+
range(1, 10),
|
|
116
|
+
itertools.repeat(args.datadir),
|
|
117
|
+
itertools.repeat(args.outdir),
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
main()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
## NOT IMPLEMENTED
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Calculate ISM shuffle scores.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pyfastx
|
|
13
|
+
import tqdm
|
|
14
|
+
from scipy.spatial.distance import cdist
|
|
15
|
+
|
|
16
|
+
import utils
|
|
17
|
+
|
|
18
|
+
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "4"
|
|
19
|
+
logging.getLogger("tensorflow").setLevel(logging.FATAL)
|
|
20
|
+
import clipnet
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
25
|
+
parser.add_argument("fasta_fp", type=str, help="Fasta file path.")
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"score_fp", type=str, help="Where to write ISM shuffle results."
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--model_dir",
|
|
31
|
+
type=str,
|
|
32
|
+
default="ensemble_models/",
|
|
33
|
+
help="directory to load models from.",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--gpu",
|
|
37
|
+
type=int,
|
|
38
|
+
default=None,
|
|
39
|
+
help="Index of GPU to use (starting from 0). If not invoked, uses CPU.",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--n_shuffles",
|
|
43
|
+
type=int,
|
|
44
|
+
default=5,
|
|
45
|
+
help="Number of shuffles/mutations to perform for each position.",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--mut_size", type=int, default=10, help="Size of mutations to use."
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--edge_padding",
|
|
52
|
+
type=int,
|
|
53
|
+
default=50,
|
|
54
|
+
help="Number of positions from edge that we'll skip mutating on.",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--corr_pseudocount",
|
|
58
|
+
type=float,
|
|
59
|
+
default=1e-6,
|
|
60
|
+
help="Pseudocount for correlation calculation.",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--log_quantity_pseudocount",
|
|
64
|
+
type=float,
|
|
65
|
+
default=1e-3,
|
|
66
|
+
help="Pseudocount for log quantity calculation.",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--seed",
|
|
70
|
+
type=int,
|
|
71
|
+
default=617,
|
|
72
|
+
help="Random seed for generating mutations.",
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--silence",
|
|
76
|
+
action="store_true",
|
|
77
|
+
help="Disables progress bars and other non-essential print statements.",
|
|
78
|
+
)
|
|
79
|
+
args = parser.parse_args()
|
|
80
|
+
np.random.seed(args.seed)
|
|
81
|
+
|
|
82
|
+
# Load models ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
83
|
+
|
|
84
|
+
nn = (
|
|
85
|
+
clipnet.CLIPNET(n_gpus=1, use_specific_gpu=args.gpu)
|
|
86
|
+
if args.gpu is not None
|
|
87
|
+
else clipnet.CLIPNET(n_gpus=0)
|
|
88
|
+
)
|
|
89
|
+
ensemble = nn.construct_ensemble(args.model_dir, silence=args.silence)
|
|
90
|
+
|
|
91
|
+
# Load sequences ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
92
|
+
|
|
93
|
+
sequences = pyfastx.Fasta(args.fasta_fp)
|
|
94
|
+
seqs_twohot = utils.get_twohot_fasta_sequences(args.fasta_fp, silence=args.silence)
|
|
95
|
+
|
|
96
|
+
# Calculate ISM shuffle scores ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
97
|
+
|
|
98
|
+
wt_pred = ensemble.predict(seqs_twohot, batch_size=256, verbose=0)
|
|
99
|
+
corr_scores = []
|
|
100
|
+
log_quantity_scores = []
|
|
101
|
+
for i in tqdm.tqdm(
|
|
102
|
+
range(len(sequences)),
|
|
103
|
+
desc="Calculating ISM shuffle scores",
|
|
104
|
+
disable=args.silence,
|
|
105
|
+
):
|
|
106
|
+
corr_score = []
|
|
107
|
+
log_quantity_score = []
|
|
108
|
+
rec = sequences[i]
|
|
109
|
+
positions = range(args.edge_padding, len(rec.seq) - args.edge_padding)
|
|
110
|
+
for shuffle in range(args.n_shuffles):
|
|
111
|
+
mutated_seqs = []
|
|
112
|
+
for pos in positions:
|
|
113
|
+
mut = utils.kshuffle(rec.seq, random_seed=args.seed)[0][: args.mut_size]
|
|
114
|
+
mutated_seq = (
|
|
115
|
+
rec.seq[0 : pos - int(len(mut) / 2)]
|
|
116
|
+
+ mut
|
|
117
|
+
+ rec.seq[pos + int(len(mut) / 2) :]
|
|
118
|
+
)
|
|
119
|
+
mutated_seqs.append(mutated_seq)
|
|
120
|
+
mut_pred = ensemble.predict(
|
|
121
|
+
np.array([utils.TwoHotDNA(seq).twohot for seq in mutated_seqs]),
|
|
122
|
+
batch_size=256,
|
|
123
|
+
verbose=0,
|
|
124
|
+
)
|
|
125
|
+
mut_corr = (
|
|
126
|
+
1
|
|
127
|
+
- cdist(
|
|
128
|
+
np.array(
|
|
129
|
+
[wt_pred[0][i, :]] * len(positions)
|
|
130
|
+
+ np.random.normal(
|
|
131
|
+
0, args.corr_pseudocount, mut_pred[0].shape[1]
|
|
132
|
+
)
|
|
133
|
+
),
|
|
134
|
+
mut_pred[0]
|
|
135
|
+
+ np.random.normal(0, args.corr_pseudocount, mut_pred[0].shape[1]),
|
|
136
|
+
metric="correlation",
|
|
137
|
+
)[0, :]
|
|
138
|
+
)
|
|
139
|
+
mut_log_quantity = np.log10(
|
|
140
|
+
mut_pred[1] + args.log_quantity_pseudocount
|
|
141
|
+
) - np.log10(wt_pred[1][i] + args.log_quantity_pseudocount)
|
|
142
|
+
corr_score.append(mut_corr)
|
|
143
|
+
log_quantity_score.append(mut_log_quantity)
|
|
144
|
+
corr_scores.append(np.array(corr_score).mean(axis=0))
|
|
145
|
+
log_quantity_scores.append(np.array(log_quantity_score).mean(axis=0))
|
|
146
|
+
|
|
147
|
+
# Save scores ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
148
|
+
|
|
149
|
+
np.savez_compressed(
|
|
150
|
+
args.score_fp,
|
|
151
|
+
corr_ism_shuffle=np.array(corr_scores),
|
|
152
|
+
log_quantity_ism_shuffle=np.array(log_quantity_scores),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
main()
|