nucleobench 0.1.0__py3-none-any.whl
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.
- nucleobench/common/__init__.py +0 -0
- nucleobench/common/argparse_lib.py +64 -0
- nucleobench/common/attribution_lib_torch.py +199 -0
- nucleobench/common/attribution_lib_torch_test.py +175 -0
- nucleobench/common/constants.py +7 -0
- nucleobench/common/gcp_utils.py +150 -0
- nucleobench/common/memory_utils.py +10 -0
- nucleobench/common/priority_queue.py +105 -0
- nucleobench/common/priority_queue_test.py +55 -0
- nucleobench/common/string_utils.py +173 -0
- nucleobench/common/string_utils_test.py +48 -0
- nucleobench/common/testing_utils.py +82 -0
- nucleobench/common/testing_utils_test.py +52 -0
- nucleobench/optimizations/__init__.py +37 -0
- nucleobench/optimizations/optimization_class.py +35 -0
- nucleobench/optimizations/utils.py +65 -0
- nucleobench/optimizations/utils_test.py +19 -0
- nucleobench-0.1.0.dist-info/METADATA +82 -0
- nucleobench-0.1.0.dist-info/RECORD +22 -0
- nucleobench-0.1.0.dist-info/WHEEL +5 -0
- nucleobench-0.1.0.dist-info/licenses/LICENSE +201 -0
- nucleobench-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Utilities for parsing arguments."""
|
|
2
|
+
|
|
3
|
+
from typing import Iterable
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import dataclasses
|
|
7
|
+
|
|
8
|
+
@dataclasses.dataclass
|
|
9
|
+
class ParsedArgs:
|
|
10
|
+
main_args: argparse.Namespace
|
|
11
|
+
model_init_args: argparse.Namespace
|
|
12
|
+
opt_init_args: argparse.Namespace
|
|
13
|
+
opt_run_args: argparse.Namespace
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_long_start_sequence(known_args: argparse.Namespace) -> argparse.Namespace:
|
|
17
|
+
"""Parse a long start sequence from a file."""
|
|
18
|
+
assert known_args.seed_sequence.startswith('local://')
|
|
19
|
+
local_fileloc = known_args.seed_sequence[len('local://'):]
|
|
20
|
+
with open(local_fileloc, 'r') as f:
|
|
21
|
+
known_args.seed_sequence = f.read()
|
|
22
|
+
return known_args
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def possibly_parse_positions_to_mutate(known_args: argparse.Namespace) -> argparse.Namespace:
|
|
26
|
+
"""Possibly parse `positions_to_mutate` from a file, or leave it untouched, depending on the value."""
|
|
27
|
+
if isinstance(known_args.positions_to_mutate, str) and known_args.positions_to_mutate.startswith('local://'):
|
|
28
|
+
local_fileloc = known_args.positions_to_mutate[len('local://'):]
|
|
29
|
+
with open(local_fileloc, 'r') as f:
|
|
30
|
+
loc_str = f.read()
|
|
31
|
+
known_args.positions_to_mutate = [int(x) for x in loc_str.split('\n')]
|
|
32
|
+
elif known_args.positions_to_mutate is None or known_args.positions_to_mutate == '' or known_args.positions_to_mutate == []:
|
|
33
|
+
known_args.positions_to_mutate = None
|
|
34
|
+
else:
|
|
35
|
+
assert isinstance(known_args.positions_to_mutate, list), (type(known_args.positions_to_mutate), known_args.positions_to_mutate)
|
|
36
|
+
known_args.positions_to_mutate = [int(x) for x in known_args.positions_to_mutate.split(',')]
|
|
37
|
+
return known_args
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def handle_leftover_args(known_args: argparse.Namespace, leftover_args: Iterable):
|
|
41
|
+
"""Handle leftover arguments, either by failing or by ignoring them."""
|
|
42
|
+
if known_args.ignore_empty_cmd_args:
|
|
43
|
+
# Check that every "value" is either `None` or `empty`. If so, allow it to continue.
|
|
44
|
+
for i in leftover_args:
|
|
45
|
+
if i.startswith('--'):
|
|
46
|
+
if '=' in i:
|
|
47
|
+
arg_val = i.split('=')[1]
|
|
48
|
+
if arg_val not in [None, '']:
|
|
49
|
+
raise ValueError(f'Unused arg, not empty: {leftover_args}')
|
|
50
|
+
continue
|
|
51
|
+
else:
|
|
52
|
+
if i not in [None, '']:
|
|
53
|
+
raise ValueError(f'Unused arg, not empty: {leftover_args}')
|
|
54
|
+
else:
|
|
55
|
+
raise ValueError(f'Unused args: {leftover_args}')
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def str_to_bool(s):
|
|
59
|
+
if s.lower() in ('yes', 'true', 't', '1'):
|
|
60
|
+
return True
|
|
61
|
+
elif s.lower() in ('no', 'false', 'f', '0'):
|
|
62
|
+
return False
|
|
63
|
+
else:
|
|
64
|
+
raise argparse.ArgumentTypeError('Boolean value expected.')
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Library for smoothgrad and genome-specific attribution methods.
|
|
2
|
+
|
|
3
|
+
Ref:
|
|
4
|
+
1. [Correcting gradient-based interpretations of deep neural networks for genomics](https://genomebiology.biomedcentral.com/articles/10.1186/s13059-023-02956-3)
|
|
5
|
+
2. [SmoothGrad: removing noise by adding noise](https://arxiv.org/abs/1706.03825)
|
|
6
|
+
3. [Quick and effective approximation of in silico saturation mutagenesis experiments with first-order taylor expansion](https://pubmed.ncbi.nlm.nih.gov/39286491/)
|
|
7
|
+
|
|
8
|
+
TODO(joelshor): Consider using public version of attribution tools, such as:
|
|
9
|
+
- PyTorch Smoothgrad: https://github.com/pkmr06/pytorch-smoothgrad
|
|
10
|
+
- PyTorch GradCam and others: https://github.com/jacobgil/pytorch-grad-cam?tab=readme-ov-file
|
|
11
|
+
- PyTorch Smoothgrad and others: https://tf-explain.readthedocs.io/en/latest/
|
|
12
|
+
|
|
13
|
+
To test locally:
|
|
14
|
+
```zsh
|
|
15
|
+
python -m nucleobench.common.attribution_lib
|
|
16
|
+
```
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import gc
|
|
20
|
+
import numpy as np
|
|
21
|
+
import torch
|
|
22
|
+
from typing import Callable, Optional
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
TISMOutputType = list[dict[str, float]]
|
|
26
|
+
SmoothgradVocabType = list[dict[str, torch.Tensor]]
|
|
27
|
+
TISMLocationsType = list[int]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def noise_inputs(
|
|
31
|
+
input_tensor: torch.Tensor,
|
|
32
|
+
noise_stdev: float,
|
|
33
|
+
times: int,
|
|
34
|
+
) -> torch.Tensor:
|
|
35
|
+
"""Generates noisy inputs.
|
|
36
|
+
|
|
37
|
+
NOTE: For simplicity, for now, we work with SINGLE TENSORS. Assume no batch dimension.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
input_tensor: Input tensor. Doesn't have to be genomic. Should NOT be batched.
|
|
41
|
+
noise_stdev: Noise to add.
|
|
42
|
+
times: Number of times to add noise.
|
|
43
|
+
"""
|
|
44
|
+
if noise_stdev < 0:
|
|
45
|
+
raise ValueError(f'Requires non-negative noise stdev: {noise_stdev}')
|
|
46
|
+
x = input_tensor # Syntactic sugar.
|
|
47
|
+
|
|
48
|
+
# Stack N versions of the input, to add uncorrelated noise to.
|
|
49
|
+
with torch.no_grad():
|
|
50
|
+
x = x.unsqueeze(0)
|
|
51
|
+
x = x.repeat([times] + [1] * (x.ndim-1))
|
|
52
|
+
|
|
53
|
+
# Add noise for hte smoothgrad algorithm.
|
|
54
|
+
if noise_stdev > 0:
|
|
55
|
+
noise_to_add = torch.normal(mean=torch.zeros(x.shape), std=noise_stdev)
|
|
56
|
+
x += noise_to_add
|
|
57
|
+
return x
|
|
58
|
+
|
|
59
|
+
def noisy_grads_torch(
|
|
60
|
+
input_tensor: torch.Tensor,
|
|
61
|
+
model: Callable[[torch.Tensor], torch.Tensor],
|
|
62
|
+
noise_stdev: float,
|
|
63
|
+
times: int,
|
|
64
|
+
idxs: Optional[TISMLocationsType] = None,
|
|
65
|
+
) -> torch.Tensor:
|
|
66
|
+
"""Generates noisy gradients from a function.
|
|
67
|
+
|
|
68
|
+
NOTE: For simplicity, for now, we work with SINGLE TENSORS. Assume no batch dimension.
|
|
69
|
+
|
|
70
|
+
This replicates the input `times` times, and runs it through the network all at once.
|
|
71
|
+
|
|
72
|
+
TODO(joelshor): Add batching, for the situation where `times` is larger than the possible batch size
|
|
73
|
+
of a single inference with a network.
|
|
74
|
+
TODO(joelshor): Add ability to efficiently compute multiple inputs at once.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
input_tensor (torch.Tensor): Input tensor. Doesn't have to be genomic. Should NOT be batched.
|
|
78
|
+
model: PyTorch model to use. The model must return a scalar per batch element.
|
|
79
|
+
noise_stdev: Noise to add.
|
|
80
|
+
times: Number of times to add noise.
|
|
81
|
+
idx: If present, only backprop through this location.
|
|
82
|
+
"""
|
|
83
|
+
x = noise_inputs(
|
|
84
|
+
input_tensor=input_tensor,
|
|
85
|
+
noise_stdev=noise_stdev,
|
|
86
|
+
times=times)
|
|
87
|
+
|
|
88
|
+
# Run inference to get grads.
|
|
89
|
+
if idxs is None:
|
|
90
|
+
x_grad = x
|
|
91
|
+
x_grad.requires_grad = True
|
|
92
|
+
else:
|
|
93
|
+
x, x_grad = apply_gradient_mask(x, idxs)
|
|
94
|
+
|
|
95
|
+
y = model(x)
|
|
96
|
+
y_sum = y.sum()
|
|
97
|
+
y_sum.backward(retain_graph=False)
|
|
98
|
+
noisy_grads = x_grad.grad.numpy()
|
|
99
|
+
|
|
100
|
+
gc.collect()
|
|
101
|
+
torch.cuda.empty_cache()
|
|
102
|
+
|
|
103
|
+
assert noisy_grads.shape == x_grad.shape
|
|
104
|
+
return noisy_grads
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def smoothgrad_torch(
|
|
108
|
+
input_tensor: torch.Tensor,
|
|
109
|
+
model: Callable[[torch.Tensor], torch.Tensor],
|
|
110
|
+
noise_stdev: float,
|
|
111
|
+
times: int,
|
|
112
|
+
idxs: Optional[TISMLocationsType] = None,
|
|
113
|
+
) -> torch.Tensor:
|
|
114
|
+
"""Custom implementation of SmoothGrad.
|
|
115
|
+
https://arxiv.org/pdf/1706.03825
|
|
116
|
+
|
|
117
|
+
NOTE: For simplicity, for now, we work with SINGLE TENSORS. Assume no batch dimension.
|
|
118
|
+
|
|
119
|
+
This replicates the input `times` times, and runs it through the network all at once.
|
|
120
|
+
|
|
121
|
+
TODO(joelshor): Add batching, for the situation where `times` is larger than the possible batch size
|
|
122
|
+
of a single inference with a network.
|
|
123
|
+
TODO(joelshor): Add ability to efficiently compute multiple inputs at once.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
input_tensor (torch.Tensor): Input tensor. Doesn't have to be genomic. Should NOT be batched.
|
|
127
|
+
model: PyTorch model to use. The model must return a scalar per batch element.
|
|
128
|
+
noise_stdev: Noise to add.
|
|
129
|
+
times: Number of times to add noise.
|
|
130
|
+
idx: If present, only backprop to this location.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Per-nucleotide smoothgrad.
|
|
134
|
+
"""
|
|
135
|
+
noisy_grads = noisy_grads_torch(
|
|
136
|
+
input_tensor=input_tensor,
|
|
137
|
+
model=model,
|
|
138
|
+
noise_stdev=noise_stdev,
|
|
139
|
+
times=times,
|
|
140
|
+
idxs=idxs,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return np.mean(noisy_grads, axis=0)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# TODO(joelshor): Add `attribution_lib.py` test, taken from `malinois/model_def_test.py`.
|
|
147
|
+
def smoothgrad_tensor_to_dict(smooth_grad: torch.Tensor, vocab: list[str]) -> SmoothgradVocabType:
|
|
148
|
+
"""Map the smoothgrad indices to the vocab."""
|
|
149
|
+
assert smooth_grad.ndim == 2
|
|
150
|
+
assert list(smooth_grad.shape)[0] == len(vocab)
|
|
151
|
+
def _to_dict(x: torch.Tensor) -> dict[str, torch.Tensor]:
|
|
152
|
+
return {vocab[i]: x[i] for i in range(len(vocab))}
|
|
153
|
+
return [_to_dict(x) for x in smooth_grad.T]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def smoothgrad_to_tism(sg: SmoothgradVocabType, base_seq: str) -> TISMOutputType:
|
|
157
|
+
"""Returns result according to Taylor in-silico mutagenesis.
|
|
158
|
+
|
|
159
|
+
Paper: https://www.cell.com/iscience/fulltext/S2589-0042(24)02032-7"""
|
|
160
|
+
assert len(sg) == len(base_seq)
|
|
161
|
+
|
|
162
|
+
tism = []
|
|
163
|
+
for base_nt, sg_dict in zip(base_seq, sg):
|
|
164
|
+
cur_tism = {}
|
|
165
|
+
for nt, sg in sg_dict.items():
|
|
166
|
+
if nt == base_nt: continue
|
|
167
|
+
cur_tism[nt] = float(sg - sg_dict[base_nt])
|
|
168
|
+
tism.append(cur_tism)
|
|
169
|
+
|
|
170
|
+
return tism
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def apply_gradient_mask(x: torch.Tensor, idxs: TISMLocationsType) -> tuple[torch.Tensor, torch.Tensor]:
|
|
174
|
+
"""Applies a gradient mask to the input tensor.
|
|
175
|
+
|
|
176
|
+
NOTE: Do NOT just multiply by 0. This will run out of memory in large models.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Tuple of (x, masked_x), where masked_x is the input tensor with the gradient mask applied.
|
|
180
|
+
"""
|
|
181
|
+
assert min(idxs) >= 0
|
|
182
|
+
assert max(idxs) < x.shape[2]
|
|
183
|
+
assert x.ndim == 3, x.shape
|
|
184
|
+
|
|
185
|
+
no_gradient = x.clone().detach()
|
|
186
|
+
no_gradient.requires_grad = False
|
|
187
|
+
|
|
188
|
+
x_grad = x[:, :, idxs].clone().detach()
|
|
189
|
+
x_grad.requires_grad = True
|
|
190
|
+
x_grad_i = {idx: i for i, idx in enumerate(idxs)}
|
|
191
|
+
|
|
192
|
+
# Instead of using `torch.where`, we use this method to make our gradient tensor
|
|
193
|
+
# as small as possible, to preserve memory.
|
|
194
|
+
tensor_slices = [x_grad[:, :, x_grad_i[i]:x_grad_i[i]+1] if i in idxs
|
|
195
|
+
else no_gradient[:, :, i:i+1]
|
|
196
|
+
for i in range(no_gradient.shape[2])]
|
|
197
|
+
x = torch.concat(tensor_slices, dim=2)
|
|
198
|
+
|
|
199
|
+
return x, x_grad
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Test attribution_lib.py.
|
|
2
|
+
|
|
3
|
+
To test:
|
|
4
|
+
pytest nucleobench/common/attribution_lib_torch_test.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import numpy as np
|
|
9
|
+
import torch
|
|
10
|
+
from torch import nn
|
|
11
|
+
|
|
12
|
+
from nucleobench.common import attribution_lib_torch
|
|
13
|
+
from nucleobench.common import testing_utils
|
|
14
|
+
|
|
15
|
+
class TestNeuralNetwork(nn.Module):
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__()
|
|
18
|
+
self.flatten = nn.Flatten()
|
|
19
|
+
self.linear_relu_stack = nn.Sequential(
|
|
20
|
+
nn.Linear(10, 3),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def forward(self, x):
|
|
24
|
+
x = self.flatten(x)
|
|
25
|
+
logits = self.linear_relu_stack(x)
|
|
26
|
+
return logits
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Nonlinear(nn.Module):
|
|
30
|
+
def __init__(self):
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.flatten = nn.Flatten()
|
|
33
|
+
self.linear_relu_stack = nn.Sequential(
|
|
34
|
+
nn.Linear(10, 2),
|
|
35
|
+
nn.Sigmoid(),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def forward(self, x):
|
|
39
|
+
x = self.flatten(x)
|
|
40
|
+
logits = self.linear_relu_stack(x)
|
|
41
|
+
return logits
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class FixedNN(nn.Module):
|
|
45
|
+
def __init__(self):
|
|
46
|
+
super().__init__()
|
|
47
|
+
self.layer_array = np.array([1.0, 2.0])
|
|
48
|
+
self.linear_layer = torch.tensor([self.layer_array], dtype=torch.float).t()
|
|
49
|
+
|
|
50
|
+
def forward(self, x):
|
|
51
|
+
logits = torch.matmul(x, self.linear_layer)
|
|
52
|
+
return logits
|
|
53
|
+
|
|
54
|
+
@pytest.mark.parametrize('times', [1, 3, 5])
|
|
55
|
+
def test_expected_gradient(times):
|
|
56
|
+
"""Regardless of number of times, noisy grads should be the same for a linear model."""
|
|
57
|
+
input_tensor = torch.randn(2)
|
|
58
|
+
model = FixedNN()
|
|
59
|
+
|
|
60
|
+
grads = attribution_lib_torch.noisy_grads_torch(
|
|
61
|
+
input_tensor=input_tensor,
|
|
62
|
+
model=model,
|
|
63
|
+
noise_stdev=1.0,
|
|
64
|
+
times=times)
|
|
65
|
+
|
|
66
|
+
assert grads.shape == (times, 2)
|
|
67
|
+
# For linear models, grads should all be the linear layer.
|
|
68
|
+
for i in range(times):
|
|
69
|
+
assert np.array_equal(grads[i, ...], model.layer_array), (
|
|
70
|
+
grads[i, ...], model.layer_array, grads)
|
|
71
|
+
|
|
72
|
+
def test_callable():
|
|
73
|
+
"""Check that additional arguments are used."""
|
|
74
|
+
input_tensor = torch.randn(2)
|
|
75
|
+
model = FixedNN()
|
|
76
|
+
|
|
77
|
+
def override_callable(x):
|
|
78
|
+
return 2.0 * model(x)
|
|
79
|
+
grads = attribution_lib_torch.noisy_grads_torch(
|
|
80
|
+
input_tensor=input_tensor,
|
|
81
|
+
model=override_callable,
|
|
82
|
+
noise_stdev=1.0,
|
|
83
|
+
times=3,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
assert grads.shape == (3, 2)
|
|
87
|
+
# For linear models, grads should all be the linear layer.
|
|
88
|
+
for i in range(3):
|
|
89
|
+
assert np.array_equal(grads[i, ...], [2.0, 4.0]), (
|
|
90
|
+
grads[i, ...], [2.0, 4.0], grads)
|
|
91
|
+
|
|
92
|
+
def test_noisy_grads():
|
|
93
|
+
input_tensor = torch.randn(10)
|
|
94
|
+
model = TestNeuralNetwork()
|
|
95
|
+
|
|
96
|
+
noisy_grads = attribution_lib_torch.noisy_grads_torch(
|
|
97
|
+
input_tensor=input_tensor,
|
|
98
|
+
model=model,
|
|
99
|
+
noise_stdev=0.25,
|
|
100
|
+
times=5)
|
|
101
|
+
assert noisy_grads.shape == (5, 10)
|
|
102
|
+
# For linear models, grads should all be the same.
|
|
103
|
+
for i in range(5):
|
|
104
|
+
assert np.array_equal(noisy_grads[i, ...], noisy_grads[0, ...])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_smoothgrad_torch():
|
|
108
|
+
input_tensor = torch.randn(10)
|
|
109
|
+
model = TestNeuralNetwork()
|
|
110
|
+
|
|
111
|
+
grad = attribution_lib_torch.smoothgrad_torch(
|
|
112
|
+
input_tensor=input_tensor,
|
|
113
|
+
model=model,
|
|
114
|
+
noise_stdev=0.25,
|
|
115
|
+
times=5)
|
|
116
|
+
|
|
117
|
+
assert grad.shape == (10,)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_noisy_grads_different():
|
|
121
|
+
"""In a nonlinear network, gradients should be different."""
|
|
122
|
+
input_tensor = torch.randn(10)
|
|
123
|
+
model = Nonlinear()
|
|
124
|
+
|
|
125
|
+
noisy_grads = attribution_lib_torch.noisy_grads_torch(
|
|
126
|
+
input_tensor=input_tensor,
|
|
127
|
+
model=model,
|
|
128
|
+
noise_stdev=1.0,
|
|
129
|
+
times=5)
|
|
130
|
+
assert noisy_grads.shape == (5, 10)
|
|
131
|
+
# For linear models, grads should all be the same.
|
|
132
|
+
for i in range(1, 5):
|
|
133
|
+
assert not np.array_equal(noisy_grads[i, ...], noisy_grads[0, ...])
|
|
134
|
+
|
|
135
|
+
# TODO(joelshor): Add a unit test for nucleotide-specific TISM.
|
|
136
|
+
def test_smoothgrad_torch_idx_sanity():
|
|
137
|
+
input_tensor = torch.randn(4, 5)
|
|
138
|
+
model = testing_utils.CountLetterModel()
|
|
139
|
+
|
|
140
|
+
grad = attribution_lib_torch.smoothgrad_torch(
|
|
141
|
+
input_tensor=input_tensor,
|
|
142
|
+
model=model.inference_on_tensor,
|
|
143
|
+
noise_stdev=0.0,
|
|
144
|
+
times=1)
|
|
145
|
+
assert grad.shape == (4, 5)
|
|
146
|
+
|
|
147
|
+
for idx in range(5):
|
|
148
|
+
grad_singlebp = attribution_lib_torch.smoothgrad_torch(
|
|
149
|
+
input_tensor=input_tensor,
|
|
150
|
+
model=model.inference_on_tensor,
|
|
151
|
+
noise_stdev=0.25,
|
|
152
|
+
times=5,
|
|
153
|
+
idxs=[idx])
|
|
154
|
+
assert grad_singlebp.shape == (4, 1)
|
|
155
|
+
assert np.all(grad[:, idx] == grad_singlebp[:, 0])
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_apply_gradient_mask():
|
|
159
|
+
"""[idx] and idx should give the same result."""
|
|
160
|
+
input_tensor = torch.randn(1, 10)
|
|
161
|
+
model = TestNeuralNetwork()
|
|
162
|
+
output_tensor = model(input_tensor)
|
|
163
|
+
output_tensor = output_tensor.reshape(1, 1, 3)
|
|
164
|
+
|
|
165
|
+
x1, x_grad = attribution_lib_torch.apply_gradient_mask(
|
|
166
|
+
output_tensor, [0])
|
|
167
|
+
assert x1.shape == (1, 1, 3)
|
|
168
|
+
assert x_grad.shape == (1, 1, 1)
|
|
169
|
+
|
|
170
|
+
x2, x_grad = attribution_lib_torch.apply_gradient_mask(
|
|
171
|
+
output_tensor, [0, 1])
|
|
172
|
+
assert x2.shape == (1, 1, 3)
|
|
173
|
+
assert x_grad.shape == (1, 1, 2)
|
|
174
|
+
|
|
175
|
+
assert (x1 == x2).all(), x1 == x2
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
VOCAB = ['A','C','G','T']
|
|
2
|
+
SERVICE_KEY_FILE_LOCATION = ''
|
|
3
|
+
GCP_OUTPUT_BUCKET_NAME = ''
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
MPRA_UPSTREAM = 'ACGAAAATGTTGGATGCTCATACTCGTCCTTTTTCAATATTATTGAAGCATTTATCAGGGTTACTAGTACGTCTCTCAAGGATAAGTAAGTAATATTAAGGTACGGGAGGTATTGGACAGGCCGCAATAAAATATCTTTATTTTCATTACATCTGTGTGTTGGTTTTTTGTGTGAATCGATAGTACTAACATACGCTCTCCATCAAAACAAAACGAAACAAAACAAACTAGCAAAATAGGCTGTCCCCAGTGCAAGTGCAGGTGCCAGAACATTTCTCTGGCCTAACTGGCCGCTTGACG'
|
|
7
|
+
MPRA_DOWNSTREAM= 'CACTGCGGCTCCTGCGATCTAACTGGCCGGTACCTGAGCTCGCTAGCCTCGAGGATATCAAGATCTGGCCTCGGCGGCCAAGCTTAGACACTAGAGGGTATATAATGGAAGCTCGACTTCCAGCTTGGCAATCCGGTACTGTTGGTAAAGCCACCATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCT'
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Utils for reading from and writing to GCP.
|
|
2
|
+
|
|
3
|
+
To test this with the cloud:
|
|
4
|
+
```zsh
|
|
5
|
+
python -m nucleobench.common.gcp_utils
|
|
6
|
+
```
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Generator
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import copy
|
|
13
|
+
import os
|
|
14
|
+
import pickle
|
|
15
|
+
import subprocess
|
|
16
|
+
import torch
|
|
17
|
+
import time
|
|
18
|
+
from google.cloud import storage
|
|
19
|
+
|
|
20
|
+
from nucleobench.common import constants
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_filepath(
|
|
24
|
+
base_dir: str,
|
|
25
|
+
opt_method: str,
|
|
26
|
+
model: str,
|
|
27
|
+
exp_start_time: str,
|
|
28
|
+
timestamp: str,
|
|
29
|
+
) -> str:
|
|
30
|
+
return os.path.join(base_dir, f'{opt_method}_{model}', exp_start_time, f'{timestamp}.pkl')
|
|
31
|
+
|
|
32
|
+
def save_proposals(
|
|
33
|
+
write_dicts: list[dict],
|
|
34
|
+
args: argparse.Namespace,
|
|
35
|
+
output_path: str,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Save the proposals and associated arguments to a file.
|
|
39
|
+
|
|
40
|
+
This function saves the generated proposals and the input arguments used for their generation
|
|
41
|
+
to a file. The file is named based on the current timestamp and a random tag. The saved file can
|
|
42
|
+
be placed in a local directory or on Google Cloud Storage if a 'gs://' path is provided in the arguments.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
write_dicts: A list of dictiomary of things to write.
|
|
46
|
+
args: Args for the job. Best to keep them close to the output.
|
|
47
|
+
output_path: Directory to write the output to, either locally or on GCP. Format of output is:
|
|
48
|
+
{output_path}/{opt_method}_{model}/{exp start time}/{reults of exp N}.pkl
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
None
|
|
52
|
+
"""
|
|
53
|
+
def _tensor2np(obj):
|
|
54
|
+
return obj.detach().clone().numpy() if isinstance(obj, torch.Tensor) else obj
|
|
55
|
+
args = {k: _tensor2np(v) for k, v in vars(args).items()}
|
|
56
|
+
write_dicts = [{k: _tensor2np(v) for k, v in x.items()} for x in write_dicts]
|
|
57
|
+
save_dicts = copy.deepcopy(write_dicts)
|
|
58
|
+
|
|
59
|
+
filename = get_filepath(
|
|
60
|
+
base_dir=output_path,
|
|
61
|
+
opt_method=args['optimization'],
|
|
62
|
+
model=args['model'],
|
|
63
|
+
exp_start_time=save_dicts[0]['exp_starttime_str'],
|
|
64
|
+
timestamp=time.strftime("%Y%m%d_%H%M%S"),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# TODO(joelshor): Used to save as torch tensors. Now, saving as a pickled dictionary
|
|
68
|
+
# of numpy arrays. Eventually, save as PyArrow arrays to parquet.
|
|
69
|
+
if filename.startswith('gs://'):
|
|
70
|
+
bucket_name = filename.split('/')[2]
|
|
71
|
+
write_str_to_gcp(
|
|
72
|
+
gcs_output_path=filename,
|
|
73
|
+
content=pickle.dumps(save_dicts),
|
|
74
|
+
binary=True,
|
|
75
|
+
bucket_name=bucket_name,
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
if os.path.dirname(filename) != '' and os.path.dirname(filename) != '.':
|
|
79
|
+
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
|
80
|
+
with open(filename, 'wb') as f:
|
|
81
|
+
pickle.dump(save_dicts, f)
|
|
82
|
+
|
|
83
|
+
print(f'Proposals deposited at:\n\t{filename}')
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_role_client(service_json_path: str = constants.SERVICE_KEY_FILE_LOCATION):
|
|
87
|
+
try:
|
|
88
|
+
gcp_client = storage.Client.from_service_account_json(service_json_path)
|
|
89
|
+
except FileNotFoundError as e:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
'GCP service key not found. You are probably trying to write output to a private '
|
|
92
|
+
'bucket. To write to your own bucket, change the bucket name and service key '
|
|
93
|
+
f'credentials in `nucleobench/common/constants.py`: {service_json_path}') from e
|
|
94
|
+
return gcp_client
|
|
95
|
+
|
|
96
|
+
def write_str_to_gcp(
|
|
97
|
+
gcs_output_path: str,
|
|
98
|
+
content: Any,
|
|
99
|
+
binary: bool,
|
|
100
|
+
bucket_name: str = constants.GCP_OUTPUT_BUCKET_NAME,
|
|
101
|
+
):
|
|
102
|
+
assert gcs_output_path.startswith('gs://'), 'gcs_output_path must be a GCS path.'
|
|
103
|
+
gcs_output_path = gcs_output_path[len('gs://'):]
|
|
104
|
+
bucket_name, blob_fn = gcs_output_path.split('/', 1)
|
|
105
|
+
|
|
106
|
+
# Instantiates a client.
|
|
107
|
+
storage_client = get_role_client()
|
|
108
|
+
bucket = storage_client.bucket(bucket_name)
|
|
109
|
+
blob = bucket.blob(blob_fn)
|
|
110
|
+
|
|
111
|
+
write_type = 'wb' if binary else 'w'
|
|
112
|
+
with blob.open(write_type) as f:
|
|
113
|
+
f.write(content)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def download_gcp_folder_to_local(dir: str, local_dir: str, bucket: str = constants.GCP_OUTPUT_BUCKET_NAME):
|
|
117
|
+
subprocess.call(['mkdir', '-p', local_dir])
|
|
118
|
+
subprocess.call(['gcloud', 'storage', 'cp', '-r', f'gs://{bucket}/{dir}', local_dir])
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def list_files_recursively(local_dir: str) -> Generator[str, None, None]:
|
|
122
|
+
"""Recursively lists all files in a given directory."""
|
|
123
|
+
for root, dirs, files in os.walk(local_dir):
|
|
124
|
+
for file in files:
|
|
125
|
+
yield os.path.join(root, file)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == '__main__':
|
|
129
|
+
args = {
|
|
130
|
+
'fake_arg': 'fake_value',
|
|
131
|
+
'fake_arg2': 'fake_value2',
|
|
132
|
+
'optimization': 'dummy',
|
|
133
|
+
'model': 'dummy_model',
|
|
134
|
+
}
|
|
135
|
+
proposal1 = {
|
|
136
|
+
'string': 'ACTC',
|
|
137
|
+
'energy': 1.0,
|
|
138
|
+
}
|
|
139
|
+
proposal2 = {
|
|
140
|
+
'string': 'ACTC',
|
|
141
|
+
'energy': 2.0,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
write_dict = {
|
|
145
|
+
'proposals': [proposal1, proposal2],
|
|
146
|
+
'exp_starttime_str': '102010',
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
save_proposals([write_dict, write_dict], argparse.Namespace(**args), './gcp_utils_test/dummy')
|
|
150
|
+
save_proposals([write_dict, write_dict], argparse.Namespace(**args), f'gs://{constants.GCP_OUTPUT_BUCKET_NAME}/gcp_utils_test/dummy')
|