ledidi 1.0.0__py3.9.egg
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.
- EGG-INFO/PKG-INFO +9 -0
- EGG-INFO/SOURCES.txt +9 -0
- EGG-INFO/dependency_links.txt +1 -0
- EGG-INFO/top_level.txt +1 -0
- EGG-INFO/zip-safe +1 -0
- ledidi/__init__.py +6 -0
- ledidi/__pycache__/__init__.cpython-39.pyc +0 -0
- ledidi/__pycache__/ledidi.cpython-39.pyc +0 -0
- ledidi/ledidi.py +227 -0
- ledidi.egg-link +2 -0
EGG-INFO/PKG-INFO
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ledidi
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Ledidi is an optimization approach for designing edits to biological sequences.
|
|
5
|
+
Home-page: http://pypi.python.org/pypi/ledidi/
|
|
6
|
+
Author: Jacob Schreiber and Yang Lu
|
|
7
|
+
Author-email: jmschreiber91@gmail.com
|
|
8
|
+
License: LICENSE.txt
|
|
9
|
+
License-File: LICENSE
|
EGG-INFO/SOURCES.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
EGG-INFO/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ledidi
|
EGG-INFO/zip-safe
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
ledidi/__init__.py
ADDED
|
Binary file
|
|
Binary file
|
ledidi/ledidi.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# ledidi.py
|
|
2
|
+
# Authors: Jacob Schreiber <jmschreiber91@gmail.com>
|
|
3
|
+
# adapted from code written by Yang Lu
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import torch
|
|
7
|
+
|
|
8
|
+
class Ledidi(torch.nn.Module):
|
|
9
|
+
"""Ledidi is a method for editing categorical sequences.
|
|
10
|
+
|
|
11
|
+
Ledidi is a method for editing categorical sequences, such as those
|
|
12
|
+
comprised of nucleotides or amino acids, to exhibit desired properties in
|
|
13
|
+
a small number of edits. It does so through the use of an oracle model,
|
|
14
|
+
which is a differentiable model that accepts a categorical sequence as
|
|
15
|
+
input and makes relevant predictions. For instance, the model might take
|
|
16
|
+
in one-hot encoded nucleotide sequence and predict the strength of binding
|
|
17
|
+
for a particular transcription factor.
|
|
18
|
+
|
|
19
|
+
Given a sequence and a desired output, Ledidi uses gradient descent to
|
|
20
|
+
design edits that bring the predicted output from the model closer to the
|
|
21
|
+
desired output. Because the sequences that predictions are being made for
|
|
22
|
+
must be categorical this involves using the Gumbel-softmax
|
|
23
|
+
reparameterization trick.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
model: torch.nn.Module
|
|
29
|
+
A model to use as an oracle that will be frozen as a part of the
|
|
30
|
+
Ledidi procedure.
|
|
31
|
+
|
|
32
|
+
shape: tuple of two integers
|
|
33
|
+
The number of categories and the number of positions, respectively,
|
|
34
|
+
in the sequence to be edited. For nucleotides this might be (4, 1000).
|
|
35
|
+
|
|
36
|
+
target: int or None
|
|
37
|
+
When given a multi-task model, the target to slice out and feed into
|
|
38
|
+
output_loss when calculating the gradient. If None, perform no slicing.
|
|
39
|
+
Default is None.
|
|
40
|
+
|
|
41
|
+
input_loss: torch.nn.Loss, optional
|
|
42
|
+
A loss to apply to the input space. By default this is the L1 loss
|
|
43
|
+
which corresponds to the number of positions that have been edited.
|
|
44
|
+
This loss is also divided by 2 to account for each edit changing
|
|
45
|
+
two values within that position. Default is torch.nn.L1Loss.
|
|
46
|
+
|
|
47
|
+
output_loss: torch.nn.Loss, optional
|
|
48
|
+
A loss to apply to the output space. By default this is the L2 loss
|
|
49
|
+
which corresponds to the mean squared error between the predicted values
|
|
50
|
+
and the desired values.
|
|
51
|
+
|
|
52
|
+
tau: float, positive, optional
|
|
53
|
+
The sharpness of the sampled values from the Gumbel distribution used
|
|
54
|
+
to generate the one-hot encodings at each step. Higher values mean
|
|
55
|
+
sharper, i.e., more closely match the argmax of each position.
|
|
56
|
+
Default is 1.
|
|
57
|
+
|
|
58
|
+
l: float, positive, optional
|
|
59
|
+
The mixing weight parameter between the input loss and the output loss,
|
|
60
|
+
applied to the output loss. The larger this value is the more important
|
|
61
|
+
it is that the output loss is minimized. Default is 100.
|
|
62
|
+
|
|
63
|
+
batch_size: int, optional
|
|
64
|
+
The number of sequences to generate at each step and average loss over.
|
|
65
|
+
Default is 32.
|
|
66
|
+
|
|
67
|
+
max_iter: int, optional
|
|
68
|
+
The maximum number of iterations to continue generating samples.
|
|
69
|
+
Default is 5000.
|
|
70
|
+
|
|
71
|
+
report_iter: int optional
|
|
72
|
+
The number of iterations to perform before reporting results of the
|
|
73
|
+
optimization. Default is 100.
|
|
74
|
+
|
|
75
|
+
lr: float, optional
|
|
76
|
+
The learning rate of the procedure. Default is 1e-2.
|
|
77
|
+
|
|
78
|
+
input_mask: torch.Tensor or None, shape=(shape[-1],)
|
|
79
|
+
A mask indicating what positions cannot be edited. This will set the
|
|
80
|
+
initial weights mask to -inf at those positions. If None, no positions
|
|
81
|
+
are masked out. Default is None.
|
|
82
|
+
|
|
83
|
+
eps: float, optional
|
|
84
|
+
The epsilon to add to the one-hot encoding. Because the first step
|
|
85
|
+
of the procedure is to take log(X + eps) the smaller eps is the
|
|
86
|
+
higher a value in the design weight needs to be achieved before
|
|
87
|
+
an edit can be induced. Default is 1e-4.
|
|
88
|
+
|
|
89
|
+
verbose: bool, optional
|
|
90
|
+
Whether to print the loss during design. Default is True.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(self, model, shape, target=None, input_loss=torch.nn.L1Loss(
|
|
94
|
+
reduction='sum'), output_loss=torch.nn.MSELoss(), tau=1, l=100,
|
|
95
|
+
batch_size=32, max_iter=5000, report_iter=100, lr=1e-2, input_mask=None,
|
|
96
|
+
eps=1e-4, verbose=True):
|
|
97
|
+
super().__init__()
|
|
98
|
+
|
|
99
|
+
for param in model.parameters():
|
|
100
|
+
param.requires_grad = False
|
|
101
|
+
|
|
102
|
+
self.model = model.eval()
|
|
103
|
+
self.input_loss = input_loss
|
|
104
|
+
self.output_loss = output_loss
|
|
105
|
+
self.tau = tau
|
|
106
|
+
self.l = l
|
|
107
|
+
self.batch_size = batch_size
|
|
108
|
+
self.max_iter = max_iter
|
|
109
|
+
self.report_iter = report_iter
|
|
110
|
+
self.lr = lr
|
|
111
|
+
self.input_mask = input_mask
|
|
112
|
+
self.eps = eps
|
|
113
|
+
self.verbose = verbose
|
|
114
|
+
|
|
115
|
+
if target is None:
|
|
116
|
+
self.target = slice(target)
|
|
117
|
+
else:
|
|
118
|
+
self.target = target
|
|
119
|
+
|
|
120
|
+
self.weights = torch.nn.Parameter(torch.zeros(1, *shape,
|
|
121
|
+
dtype=torch.float32, requires_grad=True))
|
|
122
|
+
|
|
123
|
+
def forward(self, X):
|
|
124
|
+
"""Generate a set of edits given a sequence.
|
|
125
|
+
|
|
126
|
+
This method will take in the one-hot encoded sequence and the current
|
|
127
|
+
learned weight filter and propose edits based on the Gumbel-softmax
|
|
128
|
+
distribution.
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
X: torch.Tensor, shape=(1, n_channels, length)
|
|
134
|
+
A tensor containing a single one-hot encoded sequence to propose
|
|
135
|
+
edits for. This sequence is then expanded out to the desired batch
|
|
136
|
+
size to generate a batch of edits.
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
y: torch.Tensor, shape=(batch_size, n_channels, length)
|
|
142
|
+
A tensor containing a batch of one-hot encoded sequences which
|
|
143
|
+
may contain one or more edits compared to the sequence that was
|
|
144
|
+
passed in.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
logits = torch.log(X + self.eps) + self.weights
|
|
148
|
+
logits = logits.expand(self.batch_size, *(-1 for i in range(X.ndim-1)))
|
|
149
|
+
return torch.nn.functional.gumbel_softmax(logits, tau=self.tau,
|
|
150
|
+
hard=True, dim=1)
|
|
151
|
+
|
|
152
|
+
def fit_transform(self, X, y_bar):
|
|
153
|
+
"""Appply the Ledidi procedure to design edits for a sequence.
|
|
154
|
+
|
|
155
|
+
This procedure takes in a single sequence and a desired output from
|
|
156
|
+
the model and designs edits that cause the model to predict the desired
|
|
157
|
+
output. This is done primarily by learning a weight matrix of logits
|
|
158
|
+
that can be added the log'd one-hot encoded sequence. These weights
|
|
159
|
+
are the only weights learned during the procedure.
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
X: torch.Tensor, shape=(1, n_channels, length)
|
|
165
|
+
A tensor containing a single one-hot encoded sequence to propose
|
|
166
|
+
edits for. This sequence is then expanded out to the desired batch
|
|
167
|
+
size to generate a batch of edits.
|
|
168
|
+
|
|
169
|
+
y_bar: torch.Tensor, shape=(1, *)
|
|
170
|
+
The desired output from the model. Any shape for this tensor is
|
|
171
|
+
permissable so long as the `output_loss` function can handle
|
|
172
|
+
comparing it to the output from the given model.
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
y: torch.Tensor, shape=(batch_size, n_channels, length)
|
|
178
|
+
A tensor containing a batch of one-hot encoded sequences which
|
|
179
|
+
may contain one or more edits compared to the sequence that was
|
|
180
|
+
passed in.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
optimizer = torch.optim.AdamW((self.weights,), lr=self.lr)
|
|
184
|
+
|
|
185
|
+
if self.input_mask is not None:
|
|
186
|
+
self.weights.requires_grad = False
|
|
187
|
+
self.weights.T[self.input_mask] = float("-inf")
|
|
188
|
+
self.weights[X.type(torch.bool)] = 0
|
|
189
|
+
self.weights.requires_grad = True
|
|
190
|
+
|
|
191
|
+
y_hat = self.model(X)[:, self.target]
|
|
192
|
+
|
|
193
|
+
output_loss = self.output_loss(y_hat, y_bar).item()
|
|
194
|
+
best_total_loss = self.l * output_loss
|
|
195
|
+
best_sequence = X
|
|
196
|
+
|
|
197
|
+
if self.verbose:
|
|
198
|
+
print(("iter=I\tinput_loss=0\toutput_loss={:4.4}\t" +
|
|
199
|
+
"total_loss={:4.4}").format(output_loss, best_total_loss))
|
|
200
|
+
tic = time.time()
|
|
201
|
+
|
|
202
|
+
for i in range(self.max_iter+1):
|
|
203
|
+
X_hat = self(X)
|
|
204
|
+
y_hat = self.model(X_hat)[:, self.target]
|
|
205
|
+
|
|
206
|
+
input_loss = self.input_loss(X_hat, X) / (X_hat.shape[0] * 2)
|
|
207
|
+
output_loss = self.output_loss(y_hat, y_bar)
|
|
208
|
+
|
|
209
|
+
total_loss = input_loss + self.l * output_loss
|
|
210
|
+
total_loss_ = total_loss.item()
|
|
211
|
+
|
|
212
|
+
if self.verbose and i % self.report_iter == 0:
|
|
213
|
+
print(("iter={}\tinput_loss={:4.4}\toutput_loss={:4.4}\t" +
|
|
214
|
+
"total_loss={:4.4}\ttime={:4.4}").format(i,
|
|
215
|
+
input_loss.item(), output_loss.item(),
|
|
216
|
+
total_loss_, time.time() - tic))
|
|
217
|
+
tic = time.time()
|
|
218
|
+
|
|
219
|
+
optimizer.zero_grad()
|
|
220
|
+
total_loss.backward()
|
|
221
|
+
optimizer.step()
|
|
222
|
+
|
|
223
|
+
if total_loss_ < best_total_loss:
|
|
224
|
+
best_total_loss = total_loss_
|
|
225
|
+
best_sequence = torch.clone(X_hat)
|
|
226
|
+
|
|
227
|
+
return best_sequence
|
ledidi.egg-link
ADDED