cherimoya 0.0.1__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.
- cherimoya/__init__.py +6 -0
- cherimoya/cherimoya.py +604 -0
- cherimoya/io.py +332 -0
- cherimoya/losses.py +43 -0
- cherimoya/performance.py +460 -0
- cherimoya-0.0.1.data/scripts/cherimoya +1355 -0
- cherimoya-0.0.1.dist-info/METADATA +28 -0
- cherimoya-0.0.1.dist-info/RECORD +11 -0
- cherimoya-0.0.1.dist-info/WHEEL +5 -0
- cherimoya-0.0.1.dist-info/licenses/LICENSE +21 -0
- cherimoya-0.0.1.dist-info/top_level.txt +1 -0
cherimoya/__init__.py
ADDED
cherimoya/cherimoya.py
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
# cherimoya.py
|
|
2
|
+
# Author: Jacob Schreiber <jmschreiber91@gmail.com>
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
An implementing of the Cherimoya deep learning model, which is a compact
|
|
6
|
+
architecture for predicting genomic modalities from sequence alone.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import h5py
|
|
10
|
+
import time
|
|
11
|
+
import numpy
|
|
12
|
+
import torch
|
|
13
|
+
|
|
14
|
+
import torch
|
|
15
|
+
import torch.nn as nn
|
|
16
|
+
import triton
|
|
17
|
+
import triton.language as tl
|
|
18
|
+
import itertools
|
|
19
|
+
|
|
20
|
+
from .losses import MNLLLoss
|
|
21
|
+
from .losses import log1pMSELoss
|
|
22
|
+
from .losses import _mixture_loss
|
|
23
|
+
|
|
24
|
+
from .performance import pearson_corr
|
|
25
|
+
from .performance import calculate_performance_measures
|
|
26
|
+
|
|
27
|
+
from tqdm import tqdm
|
|
28
|
+
|
|
29
|
+
from tangermeme.predict import predict
|
|
30
|
+
from bpnetlite.logging import Logger
|
|
31
|
+
|
|
32
|
+
torch.backends.cudnn.benchmark = True
|
|
33
|
+
torch.set_float32_matmul_precision('high')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def autotune_configs():
|
|
37
|
+
num_warps = [4, 8, 16]
|
|
38
|
+
num_stages = [2, 3, 4, 5]
|
|
39
|
+
BLOCK_Ls = [32, 64, 128, 256]
|
|
40
|
+
|
|
41
|
+
configs = []
|
|
42
|
+
for num_warp, num_stage, L in itertools.product(num_warps, num_stages, BLOCK_Ls):
|
|
43
|
+
configs.append(triton.Config({
|
|
44
|
+
'num_warps': num_warp,
|
|
45
|
+
'num_stages': num_stage,
|
|
46
|
+
'BLOCK_L': L
|
|
47
|
+
}))
|
|
48
|
+
return configs
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@triton.autotune(
|
|
52
|
+
configs = autotune_configs(),
|
|
53
|
+
key=['C', 'L'],
|
|
54
|
+
)
|
|
55
|
+
@triton.jit
|
|
56
|
+
def fwd_conv_kernel(
|
|
57
|
+
X_ptr, W_ptr, Y_ptr, Mean_ptr, Rstd_ptr,
|
|
58
|
+
stride_xn, dilation, eps,
|
|
59
|
+
L: tl.constexpr,
|
|
60
|
+
C: tl.constexpr,
|
|
61
|
+
BLOCK_C: tl.constexpr,
|
|
62
|
+
BLOCK_L: tl.constexpr
|
|
63
|
+
):
|
|
64
|
+
|
|
65
|
+
pid_n = tl.program_id(0)
|
|
66
|
+
offs_c = tl.arange(0, BLOCK_C)[None, :]
|
|
67
|
+
mask_c = offs_c < C
|
|
68
|
+
|
|
69
|
+
w_idx = W_ptr + offs_c
|
|
70
|
+
w0 = tl.load(w_idx, mask=mask_c, other=0.0).to(tl.float32)
|
|
71
|
+
w1 = tl.load(w_idx + C, mask=mask_c, other=0.0).to(tl.float32)
|
|
72
|
+
w2 = tl.load(w_idx + C*2, mask=mask_c, other=0.0).to(tl.float32)
|
|
73
|
+
|
|
74
|
+
running_sum = 0.0
|
|
75
|
+
running_sq_sum = 0.0
|
|
76
|
+
for l_start in tl.range(0, L, BLOCK_L):
|
|
77
|
+
offs = l_start + tl.arange(0, BLOCK_L)[:, None]
|
|
78
|
+
offs_l = offs - dilation
|
|
79
|
+
offs_r = offs + dilation
|
|
80
|
+
|
|
81
|
+
mask = (offs < L) & mask_c
|
|
82
|
+
mask_l = (offs_l >= 0) & mask
|
|
83
|
+
mask_r = (offs_r < L) & mask
|
|
84
|
+
|
|
85
|
+
x_idx = X_ptr + pid_n * stride_xn + offs_c
|
|
86
|
+
x1 = tl.load(x_idx + offs*C, mask=mask, other=0.0).to(tl.float32)
|
|
87
|
+
x0 = tl.load(x_idx + offs_l*C, mask=mask_l, other=0.0).to(tl.float32)
|
|
88
|
+
x2 = tl.load(x_idx + offs_r*C, mask=mask_r, other=0.0).to(tl.float32)
|
|
89
|
+
|
|
90
|
+
conv = x0*w0 + x1*w1 + x2*w2
|
|
91
|
+
conv2 = conv * conv
|
|
92
|
+
|
|
93
|
+
running_sum += tl.sum(conv)
|
|
94
|
+
running_sq_sum += tl.sum(conv2)
|
|
95
|
+
|
|
96
|
+
count = L * C
|
|
97
|
+
mean = running_sum / count
|
|
98
|
+
var = (running_sq_sum / count) - (mean * mean)
|
|
99
|
+
rstd = 1.0 / tl.sqrt(var + eps)
|
|
100
|
+
|
|
101
|
+
tl.store(Mean_ptr + pid_n, mean)
|
|
102
|
+
tl.store(Rstd_ptr + pid_n, rstd)
|
|
103
|
+
|
|
104
|
+
for l_start in tl.range(0, L, BLOCK_L):
|
|
105
|
+
offs = l_start + tl.arange(0, BLOCK_L)[:, None]
|
|
106
|
+
offs_l = offs - dilation
|
|
107
|
+
offs_r = offs + dilation
|
|
108
|
+
|
|
109
|
+
mask = (offs < L) & mask_c
|
|
110
|
+
mask_l = (offs_l >= 0) & mask
|
|
111
|
+
mask_r = (offs_r < L) & mask
|
|
112
|
+
|
|
113
|
+
x_idx = X_ptr + pid_n * stride_xn + offs_c
|
|
114
|
+
x1 = tl.load(x_idx + offs*C, mask=mask, other=0.0).to(tl.float32)
|
|
115
|
+
x0 = tl.load(x_idx + offs_l*C, mask=mask_l, other=0.0).to(tl.float32)
|
|
116
|
+
x2 = tl.load(x_idx + offs_r*C, mask=mask_r, other=0.0).to(tl.float32)
|
|
117
|
+
|
|
118
|
+
conv = x0*w0 + x1*w1 + x2*w2
|
|
119
|
+
x_hat = (conv - mean) * rstd
|
|
120
|
+
|
|
121
|
+
y_idx = Y_ptr + pid_n * stride_xn + offs * C + offs_c
|
|
122
|
+
tl.store(y_idx, x_hat, mask=mask)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@triton.autotune(
|
|
126
|
+
configs = autotune_configs(),
|
|
127
|
+
key=['C', 'L']
|
|
128
|
+
)
|
|
129
|
+
@triton.jit
|
|
130
|
+
def bwd_conv_kernel(
|
|
131
|
+
dY_ptr, X_ptr, W_ptr, Mean_ptr, Rstd_ptr,
|
|
132
|
+
dX_ptr, dW_ptr, stride_xn, dilation,
|
|
133
|
+
L: tl.constexpr,
|
|
134
|
+
C: tl.constexpr,
|
|
135
|
+
BLOCK_C: tl.constexpr,
|
|
136
|
+
BLOCK_L: tl.constexpr
|
|
137
|
+
):
|
|
138
|
+
pid_n = tl.program_id(0)
|
|
139
|
+
offs_c = tl.arange(0, BLOCK_C)
|
|
140
|
+
mask_c = offs_c < C
|
|
141
|
+
|
|
142
|
+
mean = tl.load(Mean_ptr + pid_n)
|
|
143
|
+
rstd = tl.load(Rstd_ptr + pid_n)
|
|
144
|
+
|
|
145
|
+
w_idx = W_ptr + offs_c
|
|
146
|
+
w0 = tl.load(w_idx, mask=mask_c)[None, :].to(tl.float32)
|
|
147
|
+
w1 = tl.load(w_idx + C, mask=mask_c)[None, :].to(tl.float32)
|
|
148
|
+
w2 = tl.load(w_idx + C*2, mask=mask_c)[None, :].to(tl.float32)
|
|
149
|
+
|
|
150
|
+
sum_dy = 0.0
|
|
151
|
+
sum_dy_xhat = 0.0
|
|
152
|
+
|
|
153
|
+
mask_c = mask_c[None, :]
|
|
154
|
+
offs_c = offs_c[None, :]
|
|
155
|
+
|
|
156
|
+
for l_start in tl.range(0, L, BLOCK_L):
|
|
157
|
+
offs = l_start + tl.arange(0, BLOCK_L)[:, None]
|
|
158
|
+
offs_l = offs - dilation
|
|
159
|
+
offs_r = offs + dilation
|
|
160
|
+
|
|
161
|
+
mask = (offs < L) & mask_c
|
|
162
|
+
mask_l = (offs_l >= 0) & mask
|
|
163
|
+
mask_r = (offs_r < L) & mask
|
|
164
|
+
|
|
165
|
+
x_idx = X_ptr + pid_n * stride_xn + offs_c
|
|
166
|
+
x1 = tl.load(x_idx + offs*C, mask=mask, other=0.0).to(tl.float32)
|
|
167
|
+
x0 = tl.load(x_idx + offs_l*C, mask=mask_l, other=0.0).to(tl.float32)
|
|
168
|
+
x2 = tl.load(x_idx + offs_r*C, mask=mask_r, other=0.0).to(tl.float32)
|
|
169
|
+
|
|
170
|
+
conv = x0*w0 + x1*w1 + x2*w2
|
|
171
|
+
x_hat = (conv - mean) * rstd
|
|
172
|
+
|
|
173
|
+
y_idx = dY_ptr + pid_n * stride_xn + offs * C + offs_c
|
|
174
|
+
dy = tl.load(y_idx, mask=mask, other=0.0).to(tl.float32)
|
|
175
|
+
|
|
176
|
+
sum_dy += tl.sum(dy)
|
|
177
|
+
sum_dy_xhat += tl.sum(dy * x_hat)
|
|
178
|
+
|
|
179
|
+
dw0 = tl.zeros((1, BLOCK_C), dtype=tl.float32)
|
|
180
|
+
dw1 = tl.zeros((1, BLOCK_C), dtype=tl.float32)
|
|
181
|
+
dw2 = tl.zeros((1, BLOCK_C), dtype=tl.float32)
|
|
182
|
+
|
|
183
|
+
for l_start in tl.range(0, L, BLOCK_L):
|
|
184
|
+
offs = l_start + tl.arange(0, BLOCK_L)[:, None]
|
|
185
|
+
offs_l = offs - dilation
|
|
186
|
+
offs_r = offs + dilation
|
|
187
|
+
|
|
188
|
+
mask = (offs < L) & mask_c
|
|
189
|
+
mask_l = (offs_l >= 0) & mask
|
|
190
|
+
mask_r = (offs_r < L) & mask
|
|
191
|
+
|
|
192
|
+
x_idx = X_ptr + pid_n * stride_xn + offs_c
|
|
193
|
+
x1 = tl.load(x_idx + offs*C, mask=mask, other=0.0).to(tl.float32)
|
|
194
|
+
x0 = tl.load(x_idx + offs_l*C, mask=mask_l, other=0.0).to(tl.float32)
|
|
195
|
+
x2 = tl.load(x_idx + offs_r*C, mask=mask_r, other=0.0).to(tl.float32)
|
|
196
|
+
|
|
197
|
+
conv = x0*w0 + x1*w1 + x2*w2
|
|
198
|
+
x_hat = (conv - mean) * rstd
|
|
199
|
+
|
|
200
|
+
###
|
|
201
|
+
|
|
202
|
+
dy_idx = dY_ptr + pid_n * stride_xn + offs * C + offs_c
|
|
203
|
+
dy = tl.load(dy_idx, mask=mask, other=0.0).to(tl.float32)
|
|
204
|
+
|
|
205
|
+
count = L * C
|
|
206
|
+
d_conv = (rstd / count) * (count * dy - sum_dy - x_hat * sum_dy_xhat)
|
|
207
|
+
|
|
208
|
+
dw0 += tl.sum(d_conv * x0, axis=0)[None, :]
|
|
209
|
+
dw1 += tl.sum(d_conv * x1, axis=0)[None, :]
|
|
210
|
+
dw2 += tl.sum(d_conv * x2, axis=0)[None, :]
|
|
211
|
+
|
|
212
|
+
dx_idx0 = dX_ptr + pid_n * stride_xn + offs * C + offs_c
|
|
213
|
+
|
|
214
|
+
dx1 = tl.load(dx_idx0, mask=mask, other=0.0)
|
|
215
|
+
dx0 = tl.load(dx_idx0 - dilation*C, mask=mask_l, other=0.0)
|
|
216
|
+
dx2 = tl.load(dx_idx0 + dilation*C, mask=mask_r, other=0.0)
|
|
217
|
+
|
|
218
|
+
dx1 += d_conv * w1
|
|
219
|
+
dx0 += d_conv * w0
|
|
220
|
+
dx2 += d_conv * w2
|
|
221
|
+
|
|
222
|
+
tl.store(dx_idx0, dx1, mask=mask)
|
|
223
|
+
tl.store(dx_idx0 - dilation*C, dx0, mask=mask_l)
|
|
224
|
+
tl.store(dx_idx0 + dilation*C, dx2, mask=mask_r)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
dw_idx = dW_ptr + pid_n * (C * 3) + offs_c
|
|
228
|
+
tl.store(dw_idx, dw0, mask=mask_c)
|
|
229
|
+
tl.store(dw_idx + C, dw1, mask=mask_c)
|
|
230
|
+
tl.store(dw_idx + C*2, dw2, mask=mask_c)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class FusedDilatedConvNormFunc(torch.autograd.Function):
|
|
234
|
+
@staticmethod
|
|
235
|
+
def forward(ctx, x, w, dilation):
|
|
236
|
+
N, L, C = x.shape
|
|
237
|
+
BLOCK_C = triton.next_power_of_2(C)
|
|
238
|
+
eps = 1e-3
|
|
239
|
+
|
|
240
|
+
mean = torch.empty((N,), dtype=torch.float32, device=x.device)
|
|
241
|
+
rstd = torch.empty((N,), dtype=torch.float32, device=x.device)
|
|
242
|
+
y = torch.empty_like(x)
|
|
243
|
+
|
|
244
|
+
fwd_conv_kernel[(N,)](
|
|
245
|
+
x, w, y, mean, rstd,
|
|
246
|
+
x.stride(0), dilation, eps,
|
|
247
|
+
L, C, BLOCK_C=BLOCK_C
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
ctx.save_for_backward(x, w, mean, rstd)
|
|
251
|
+
ctx.dilation = dilation
|
|
252
|
+
return y
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def backward(ctx, dy):
|
|
256
|
+
x, w, mean, rstd = ctx.saved_tensors
|
|
257
|
+
N, L, C = x.shape
|
|
258
|
+
BLOCK_C = triton.next_power_of_2(C)
|
|
259
|
+
|
|
260
|
+
dy = dy.contiguous()
|
|
261
|
+
dx = torch.zeros_like(x, dtype=x.dtype)
|
|
262
|
+
dw = torch.empty((N, 3, C), device=x.device, dtype=torch.float32)
|
|
263
|
+
|
|
264
|
+
bwd_conv_kernel[(N,)](
|
|
265
|
+
dy, x, w, mean, rstd, dx, dw,
|
|
266
|
+
x.stride(0), ctx.dilation,
|
|
267
|
+
L, C, BLOCK_C,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
dw = dw.sum(dim=0)
|
|
271
|
+
return dx.to(x.dtype), dw.to(x.dtype), None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
###
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class CheriBlock(torch.nn.Module):
|
|
278
|
+
def __init__(self, n_filters, dilation, eps=0.01):
|
|
279
|
+
super().__init__()
|
|
280
|
+
self.n_filters = n_filters
|
|
281
|
+
self.dilation = dilation
|
|
282
|
+
|
|
283
|
+
self.conv_weight = torch.nn.Parameter(torch.randn(3, n_filters))
|
|
284
|
+
self.linear1 = torch.nn.Linear(n_filters, 2*n_filters, bias=False)
|
|
285
|
+
self.linear2 = torch.nn.Linear(2*n_filters, n_filters, bias=False)
|
|
286
|
+
self.gamma = torch.nn.Parameter(torch.ones(1, n_filters) * eps)
|
|
287
|
+
self.activation = torch.nn.GELU(approximate='tanh')
|
|
288
|
+
|
|
289
|
+
torch.nn.init.trunc_normal_(self.conv_weight, std=0.02)
|
|
290
|
+
torch.nn.init.trunc_normal_(self.linear1.weight, std=0.02)
|
|
291
|
+
torch.nn.init.trunc_normal_(self.linear2.weight, std=0.02)
|
|
292
|
+
|
|
293
|
+
def forward(self, X):
|
|
294
|
+
X_conv = FusedDilatedConvNormFunc.apply(X, self.conv_weight, self.dilation)
|
|
295
|
+
X_mlp = self.linear2(self.activation(self.linear1(X_conv)))
|
|
296
|
+
return X + X_mlp * self.gamma
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class CheriBlock2(torch.nn.Module):
|
|
300
|
+
def __init__(self, n_filters, dilation, eps=0.01):
|
|
301
|
+
super().__init__()
|
|
302
|
+
self.n_filters = n_filters
|
|
303
|
+
self.dilation = dilation
|
|
304
|
+
|
|
305
|
+
self.conv = torch.nn.Conv1d(n_filters, n_filters, groups=n_filters, dilation=dilation, padding=dilation, kernel_size=3)
|
|
306
|
+
#self.norm = torch.nn.LayerNorm((n_filters, 2114), elementwise_affine=False, bias=False, eps=1e-3)
|
|
307
|
+
self.linear1 = torch.nn.Conv1d(n_filters, 3*n_filters, kernel_size=1, bias=False)
|
|
308
|
+
self.linear2 = torch.nn.Conv1d(3*n_filters, n_filters, kernel_size=1, bias=False)
|
|
309
|
+
self.gamma = torch.nn.Parameter(torch.ones(n_filters, 1) * eps)
|
|
310
|
+
self.activation = torch.nn.GELU(approximate='tanh')
|
|
311
|
+
|
|
312
|
+
torch.nn.init.trunc_normal_(self.conv.weight, std=0.02)
|
|
313
|
+
torch.nn.init.trunc_normal_(self.linear1.weight, std=0.02)
|
|
314
|
+
torch.nn.init.trunc_normal_(self.linear2.weight, std=0.02)
|
|
315
|
+
|
|
316
|
+
def forward(self, X):
|
|
317
|
+
X_conv = self.conv(X)
|
|
318
|
+
X_conv = self.norm(X_conv)
|
|
319
|
+
X_mlp = self.linear2(self.activation(self.linear1(X_conv)))
|
|
320
|
+
X = torch.add(X, X_mlp * self.gamma)
|
|
321
|
+
return X
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class Cherimoya(torch.nn.Module):
|
|
326
|
+
def __init__(self, n_filters=64, n_layers=9, n_outputs=1,
|
|
327
|
+
n_control_tracks=0, name=None, trimming=None,
|
|
328
|
+
single_count_output=True, verbose=True):
|
|
329
|
+
super(Cherimoya, self).__init__()
|
|
330
|
+
self.n_filters = n_filters
|
|
331
|
+
self.n_layers = n_layers
|
|
332
|
+
self.n_outputs = n_outputs
|
|
333
|
+
self.n_control_tracks = n_control_tracks
|
|
334
|
+
|
|
335
|
+
self.name = name or "cherimoya.{}.{}".format(n_filters, n_layers)
|
|
336
|
+
self.trimming = trimming or 46 + sum(2**i for i in range(n_layers))
|
|
337
|
+
|
|
338
|
+
self.iconv = torch.nn.Conv1d(4, n_filters, kernel_size=19, padding=9)
|
|
339
|
+
self.igelu = torch.nn.GELU(approximate='tanh')
|
|
340
|
+
|
|
341
|
+
self.blocks = torch.nn.ModuleList([
|
|
342
|
+
CheriBlock(n_filters, 2**i) for i in range(self.n_layers)
|
|
343
|
+
])
|
|
344
|
+
|
|
345
|
+
self.fconv = torch.nn.Conv1d(n_filters+n_control_tracks, n_outputs,
|
|
346
|
+
kernel_size=75, padding=37)
|
|
347
|
+
|
|
348
|
+
self.lw0 = torch.nn.Parameter(torch.ones(1))
|
|
349
|
+
self.lw1 = torch.nn.Parameter(torch.ones(1))
|
|
350
|
+
|
|
351
|
+
n_count_control = 1 if n_control_tracks > 0 else 0
|
|
352
|
+
n_count_outputs = 1 if single_count_output else n_outputs
|
|
353
|
+
self.linear = torch.nn.Linear(n_filters+n_count_control, n_count_outputs)
|
|
354
|
+
|
|
355
|
+
torch.nn.init.trunc_normal_(self.iconv.weight, std=0.02)
|
|
356
|
+
torch.nn.init.trunc_normal_(self.fconv.weight, std=0.02)
|
|
357
|
+
torch.nn.init.trunc_normal_(self.linear.weight, std=0.02)
|
|
358
|
+
|
|
359
|
+
torch.nn.init.zeros_(self.iconv.bias)
|
|
360
|
+
torch.nn.init.zeros_(self.fconv.bias)
|
|
361
|
+
torch.nn.init.zeros_(self.linear.bias)
|
|
362
|
+
|
|
363
|
+
self.logger = Logger(["Epoch", "Iteration", "Training Time",
|
|
364
|
+
"Validation Time", "Training MNLL", "Training Count MSE",
|
|
365
|
+
"Validation MNLL", "Validation Profile Pearson",
|
|
366
|
+
"Validation Count Pearson", "Validation Count MSE", "Saved?"],
|
|
367
|
+
verbose=verbose)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@torch.compile(mode='max-autotune')
|
|
371
|
+
def forward(self, X, X_ctl=None):
|
|
372
|
+
"""A forward pass of the model.
|
|
373
|
+
|
|
374
|
+
This method takes in a nucleotide sequence X, a corresponding
|
|
375
|
+
per-position value from a control track, and a per-locus value
|
|
376
|
+
from the control track and makes predictions for the profile
|
|
377
|
+
and for the counts. This per-locus value is usually the
|
|
378
|
+
log(sum(X_ctl_profile)+1) when the control is an experimental
|
|
379
|
+
read track but can also be the output from another model.
|
|
380
|
+
|
|
381
|
+
Parameters
|
|
382
|
+
----------
|
|
383
|
+
X: torch.tensor, shape=(batch_size, 4, length)
|
|
384
|
+
The one-hot encoded batch of sequences.
|
|
385
|
+
|
|
386
|
+
X_ctl: torch.tensor or None, shape=(batch_size, n_strands, length)
|
|
387
|
+
A value representing the signal of the control at each position in
|
|
388
|
+
the sequence. If no controls, pass in None. Default is None.
|
|
389
|
+
|
|
390
|
+
Returns
|
|
391
|
+
-------
|
|
392
|
+
y_profile: torch.tensor, shape=(batch_size, n_strands, out_length)
|
|
393
|
+
The output predictions for each strand trimmed to the output
|
|
394
|
+
length.
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
start, end = self.trimming, X.shape[2] - self.trimming
|
|
398
|
+
|
|
399
|
+
X = self.igelu(self.iconv(X))
|
|
400
|
+
X = X.transpose(1, 2).contiguous()
|
|
401
|
+
for i in range(self.n_layers):
|
|
402
|
+
X = self.blocks[i](X)
|
|
403
|
+
|
|
404
|
+
X = X.transpose(1, 2).contiguous()
|
|
405
|
+
if X_ctl is None:
|
|
406
|
+
X_w_ctl = X
|
|
407
|
+
else:
|
|
408
|
+
X_w_ctl = torch.cat([X, X_ctl], dim=1)
|
|
409
|
+
|
|
410
|
+
y_profile = self.fconv(X_w_ctl)[:, :, start:end]
|
|
411
|
+
|
|
412
|
+
# counts prediction
|
|
413
|
+
X = torch.mean(X[:, :, start-37:end+37].float(), dim=2)
|
|
414
|
+
if X_ctl is not None:
|
|
415
|
+
X_ctl = torch.sum(X_ctl[:, :, start-37:end+37].float(), dim=(1, 2))
|
|
416
|
+
X_ctl = X_ctl.unsqueeze(-1)
|
|
417
|
+
X = torch.cat([X, torch.log(X_ctl+1)], dim=-1)
|
|
418
|
+
|
|
419
|
+
y_counts = self.linear(X)
|
|
420
|
+
return y_profile, y_counts
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def fit(self, training_data, muon_optimizer, adam_optimizer, muon_scheduler,
|
|
424
|
+
adam_scheduler, X_valid, X_ctl_valid, y_valid, max_epochs=100, batch_size=64,
|
|
425
|
+
dtype='float32', device='cuda', early_stopping=None):
|
|
426
|
+
"""Fit the model to data and validate it periodically.
|
|
427
|
+
|
|
428
|
+
This method controls the training of a BPNet model. It will fit the
|
|
429
|
+
model to examples generated by the `training_data` DataLoader object
|
|
430
|
+
and, if validation data is provided, will validate the model against
|
|
431
|
+
it at the end of each epoch and return those values.
|
|
432
|
+
|
|
433
|
+
Two versions of the model will be saved: the best model found during
|
|
434
|
+
training according to the validation measures, and the final model
|
|
435
|
+
at the end of training. Additionally, a log will be saved of the
|
|
436
|
+
training and validation statistics, e.g. time and performance.
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
Parameters
|
|
440
|
+
----------
|
|
441
|
+
training_data: torch.utils.data.DataLoader
|
|
442
|
+
A generator that produces examples to train on. If n_control_tracks
|
|
443
|
+
is greater than 0, must product two inputs, otherwise must produce
|
|
444
|
+
only one input.
|
|
445
|
+
|
|
446
|
+
muon_optimizer: torch.optim.Optimizer
|
|
447
|
+
A Muon optimizer to control the training of the 2D non-head/non-tail layers
|
|
448
|
+
in the model. This is mostly the dense layers and depth-wise convolutions of
|
|
449
|
+
the Cheri blocks.
|
|
450
|
+
|
|
451
|
+
adam_optimizer: torch.optim.Optimizer
|
|
452
|
+
An Adam/W optimizer to control the training of the other parametrers. This
|
|
453
|
+
should be the head/tail layers, the bias terms, and any other parameters
|
|
454
|
+
that are not 2D matrices.
|
|
455
|
+
|
|
456
|
+
muon_scheduler: torch.optim.lr_scheduler
|
|
457
|
+
The scheduler to use for the Muon optimizer. This should likely be a cosine
|
|
458
|
+
decay with a warmup phase.
|
|
459
|
+
|
|
460
|
+
adam_scheduler: torch.optim.lr_scheduler
|
|
461
|
+
The scheduler to use for the Adam/W optimizer. This should likely be the
|
|
462
|
+
same cosine decay with a warmup phase used for the Muon optimizer.
|
|
463
|
+
|
|
464
|
+
X_valid: torch.tensor, shape=(n, 4, length)
|
|
465
|
+
A block of sequences to validate on at the end of each epoch.
|
|
466
|
+
|
|
467
|
+
X_ctl_valid: torch.tensor or None, shape=(n, n_control_tracks, length)
|
|
468
|
+
A block of control sequences to use for making the validation set
|
|
469
|
+
predictions at the end of each epoch. If n_control_tracks is None, pass in
|
|
470
|
+
None. Default is None.
|
|
471
|
+
|
|
472
|
+
y_valid: torch.tensor or None, shape=(n, n_outputs, output_length)
|
|
473
|
+
A block of signals to validate against at the end of each epochs.
|
|
474
|
+
|
|
475
|
+
max_epochs: int
|
|
476
|
+
The maximum number of epochs to train for, as measured by the
|
|
477
|
+
number of times that `training_data` is exhausted. Default is 100.
|
|
478
|
+
|
|
479
|
+
batch_size: int, optional
|
|
480
|
+
The number of examples to include in each batch. Default is 64.
|
|
481
|
+
|
|
482
|
+
dtype: str or torch.dtype
|
|
483
|
+
The torch.dtype to use when training. Usually, this will be torch.float32
|
|
484
|
+
or torch.bfloat16. Default is torch.float32.
|
|
485
|
+
|
|
486
|
+
device: str
|
|
487
|
+
The device to use for training and inference. Typically, this will be
|
|
488
|
+
'cuda' but can be anything supported by torch. Default is 'cuda'.
|
|
489
|
+
|
|
490
|
+
early_stopping: int or None, optional
|
|
491
|
+
Whether to stop training early. If None, continue training until
|
|
492
|
+
max_epochs is reached. If an integer, continue training until that
|
|
493
|
+
number of epochs has been hit without improvement in performance.
|
|
494
|
+
Default is None.
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
if X_valid is not None:
|
|
498
|
+
y_valid_counts = y_valid.sum(dim=2)
|
|
499
|
+
|
|
500
|
+
if X_ctl_valid is not None:
|
|
501
|
+
X_ctl_valid = (X_ctl_valid,)
|
|
502
|
+
|
|
503
|
+
dtype = getattr(torch, dtype) if isinstance(dtype, str) else dtype
|
|
504
|
+
|
|
505
|
+
iteration = 0
|
|
506
|
+
early_stop_count = 0
|
|
507
|
+
best_loss = float("inf")
|
|
508
|
+
self.logger.start()
|
|
509
|
+
|
|
510
|
+
###
|
|
511
|
+
|
|
512
|
+
for epoch in range(max_epochs):
|
|
513
|
+
tic = time.time()
|
|
514
|
+
|
|
515
|
+
for data in training_data:
|
|
516
|
+
X, y, labels = data[0], data[-2], data[-1]
|
|
517
|
+
X_ctl = data[1].to(device) if len(data) == 4 else None
|
|
518
|
+
|
|
519
|
+
if X.shape[0] != batch_size:
|
|
520
|
+
continue
|
|
521
|
+
|
|
522
|
+
X = X.to(device).float()
|
|
523
|
+
y = y.to(device)
|
|
524
|
+
|
|
525
|
+
# Clear the optimizer and set the model to training mode
|
|
526
|
+
muon_optimizer.zero_grad()
|
|
527
|
+
adam_optimizer.zero_grad()
|
|
528
|
+
self.train()
|
|
529
|
+
|
|
530
|
+
# Make one training step
|
|
531
|
+
with torch.autocast(device_type=device, dtype=dtype):
|
|
532
|
+
y_hat_logits, y_hat_logcounts = self(X, X_ctl)
|
|
533
|
+
|
|
534
|
+
profile_loss, count_loss = _mixture_loss(y, y_hat_logits.float(),
|
|
535
|
+
y_hat_logcounts.float(), )
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
w0 = (1.0 / (2.0 * self.lw0 ** 2))
|
|
539
|
+
w1 = (1.0 / (2.0 * self.lw1 ** 2))
|
|
540
|
+
loss = w0*profile_loss + w1*count_loss
|
|
541
|
+
|
|
542
|
+
if self.lw0.requires_grad == True:
|
|
543
|
+
loss += torch.sum(torch.log(self.lw0) ** 2 + torch.log(self.lw1) ** 2)
|
|
544
|
+
|
|
545
|
+
loss.backward()
|
|
546
|
+
|
|
547
|
+
muon_optimizer.step()
|
|
548
|
+
adam_optimizer.step()
|
|
549
|
+
|
|
550
|
+
muon_scheduler.step()
|
|
551
|
+
adam_scheduler.step()
|
|
552
|
+
|
|
553
|
+
iteration += 1
|
|
554
|
+
|
|
555
|
+
train_time = time.time() - tic
|
|
556
|
+
|
|
557
|
+
if self.lw0.requires_grad == True and torch.abs(self.lw0.grad).sum() < 1:
|
|
558
|
+
self.lw0.requires_grad = False
|
|
559
|
+
self.lw1.requires_grad = False
|
|
560
|
+
|
|
561
|
+
# Validate the model at the end of the epoch
|
|
562
|
+
with torch.no_grad():
|
|
563
|
+
self.eval()
|
|
564
|
+
tic = time.time()
|
|
565
|
+
|
|
566
|
+
y_hat_logits, y_hat_logcounts = predict(self, X_valid, args=X_ctl_valid,
|
|
567
|
+
batch_size=batch_size, dtype=dtype, device=device)
|
|
568
|
+
|
|
569
|
+
valid_profile_loss, valid_count_loss = _mixture_loss(y_valid,
|
|
570
|
+
y_hat_logits, y_hat_logcounts)
|
|
571
|
+
|
|
572
|
+
valid_loss = w0*valid_profile_loss + w1*valid_count_loss
|
|
573
|
+
|
|
574
|
+
measures = calculate_performance_measures(y_hat_logits,
|
|
575
|
+
y_valid, y_hat_logcounts, measures=['profile_pearson', 'count_pearson'])
|
|
576
|
+
|
|
577
|
+
valid_profile_corr = numpy.nan_to_num(measures['profile_pearson'])
|
|
578
|
+
valid_count_corr = numpy.nan_to_num(measures['count_pearson'])
|
|
579
|
+
valid_time = time.time() - tic
|
|
580
|
+
|
|
581
|
+
self.logger.add([epoch,
|
|
582
|
+
iteration,
|
|
583
|
+
train_time,
|
|
584
|
+
valid_time,
|
|
585
|
+
profile_loss.item(),
|
|
586
|
+
count_loss.item(),
|
|
587
|
+
valid_profile_loss.item(),
|
|
588
|
+
valid_profile_corr.mean(),
|
|
589
|
+
valid_count_corr.mean(),
|
|
590
|
+
valid_count_loss.item(),
|
|
591
|
+
(valid_loss < best_loss).item()])
|
|
592
|
+
|
|
593
|
+
self.logger.save("{}.log".format(self.name))
|
|
594
|
+
|
|
595
|
+
if valid_loss < best_loss:
|
|
596
|
+
torch.save(self, "{}.torch".format(self.name))
|
|
597
|
+
best_loss = valid_loss
|
|
598
|
+
early_stop_count = -1
|
|
599
|
+
|
|
600
|
+
early_stop_count += 1
|
|
601
|
+
if early_stopping is not None and early_stop_count >= early_stopping:
|
|
602
|
+
break
|
|
603
|
+
|
|
604
|
+
torch.save(self, "{}.final.torch".format(self.name))
|