cherimoya 0.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jacob Schreiber
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.
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: cherimoya
3
+ Version: 0.0.1
4
+ Summary: A light-weight deep learning architecture that uses modern optimization tricks to achieve strong predictive performance.
5
+ Home-page: https://github.com/jmschrei/cherimoya
6
+ Author: Jacob Schreiber
7
+ Author-email: jmschreiber91@gmail.com
8
+ License: LICENSE.txt
9
+ License-File: LICENSE
10
+ Requires-Dist: numpy>=1.14.2
11
+ Requires-Dist: scipy>=1.0.0
12
+ Requires-Dist: pandas>=1.3.3
13
+ Requires-Dist: torch>=1.9.0
14
+ Requires-Dist: h5py>=3.7.0
15
+ Requires-Dist: tqdm>=4.64.1
16
+ Requires-Dist: seaborn>=0.11.2
17
+ Requires-Dist: modisco>=2.0.0
18
+ Requires-Dist: tangermeme>=0.2.3
19
+ Requires-Dist: macs3
20
+ Requires-Dist: bam2bw
21
+ Requires-Dist: bpnet-lite
22
+ Dynamic: author
23
+ Dynamic: author-email
24
+ Dynamic: home-page
25
+ Dynamic: license
26
+ Dynamic: license-file
27
+ Dynamic: requires-dist
28
+ Dynamic: summary
@@ -0,0 +1,40 @@
1
+ <img src="https://github.com/jmschrei/cherimoya/blob/main/imgs/cherimoya.png">
2
+
3
+ > [!IMPORTANT]
4
+ > Cherimoya is still under active development and may change in ways that are not back compatible. Please make note of the version you are using in case you need to return to it in the future.
5
+
6
+ Cherimoya is a lightweight genomic sequence-to-function (S2F) model for predicting genomic modalities such as transcription factor binding, chromatin accessibility, and transcription initiation. It builds on concepts that were first introduced by BPNet and ChromBPNet while introducing architectural, algorithmic, and systems-level improvements that improve training stability, efficiency, and predictive performance. Despite needing significantly fewer parameters than other architectures, Cherimoya achieves strong predictive performance across a range of tasks and runs ~5-15x faster when measured on an H200 GPU.
7
+
8
+ <img src="https://github.com/jmschrei/cherimoya/blob/main/imgs/cheri-model.png">
9
+
10
+ The secret to Cherimoya's success is a new Cheri Block, which adapts the ConvNeXT block to the domain of noisy high-throughput genomics experiments. This block is comprised of a dilated depth-wise convolution, a layer norm, a projection into a higher-dimensional space, a GeLU non-linearity, a projection back into the original dimensionality, and then a channel-wise scaling for robustness. Conceptually, this means that the blocks first aggregate information spatially but independently for each feature/channel (the depth-wise convolution) and then aggregate information across features but independently for each position (the two projections). The dilated depth-wise convolution and the layer norm have been fused into an efficient custom GPU kernel that is ~2-3x faster than the native PyTorch implementation.
11
+
12
+ <img src="https://github.com/jmschrei/cherimoya/blob/main/imgs/cheri-block.png">
13
+
14
+ ---
15
+
16
+ ## Key Features
17
+
18
+ ### Lightweight Architecture
19
+ Cherimoya employs a compact convolutional backbone that substantially reduces parameter count without sacrificing predictive accuracy. This design enables efficient training, large-scale hyperparameter exploration, interactive usage via browsers, and usage of dozens or hundreds of such models simultaneously in complex design settings.
20
+
21
+ ### Automatic Loss Weight Balancing
22
+ Profile and count losses are combined using learned weighting parameters rather than fixed hyperparameters. This approach replaces the heuristic developed for BPNet and ChromBPNet models and enables the models to scale to larger contexts and across modalities automatically, while also improving gradient stability across datasets with varying signal-to-noise characteristics.
23
+
24
+ ### Muon Optimizer
25
+ Cherimoya uses the Muon optimizer when training the projection layers, and the AdamW optimizer for all other layers. This has significantly accelerated training by reducing the number of epochs needed while modestly improving performance.
26
+
27
+ ### End-to-End Pipeline
28
+ Cherimoya provides an integrated pipeline covering:
29
+
30
+ - BAM/SAM/fragment file conversion using bam2bw
31
+ - Peak calling using MACS3
32
+ - model training
33
+ - evaluation
34
+ - downstream analysis and motif discovery using TF-MoDISco
35
+
36
+ <img src="https://github.com/jmschrei/cherimoya/blob/main/imgs/pipeline.png">
37
+
38
+
39
+ This design supports reproducible end-to-end experiments and reduces the overhead associated with managing separate tooling for each stage.
40
+
@@ -0,0 +1,6 @@
1
+ # cherimoya
2
+ # Author: Jacob Schreiber
3
+
4
+ from .cherimoya import Cherimoya
5
+
6
+ __version__ = '0.0.1'
@@ -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))