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.
- cherimoya-0.0.1/LICENSE +21 -0
- cherimoya-0.0.1/PKG-INFO +28 -0
- cherimoya-0.0.1/README.md +40 -0
- cherimoya-0.0.1/cherimoya/__init__.py +6 -0
- cherimoya-0.0.1/cherimoya/cherimoya.py +604 -0
- cherimoya-0.0.1/cherimoya/io.py +332 -0
- cherimoya-0.0.1/cherimoya/losses.py +43 -0
- cherimoya-0.0.1/cherimoya/performance.py +460 -0
- cherimoya-0.0.1/cherimoya.egg-info/PKG-INFO +28 -0
- cherimoya-0.0.1/cherimoya.egg-info/SOURCES.txt +14 -0
- cherimoya-0.0.1/cherimoya.egg-info/dependency_links.txt +1 -0
- cherimoya-0.0.1/cherimoya.egg-info/requires.txt +12 -0
- cherimoya-0.0.1/cherimoya.egg-info/top_level.txt +1 -0
- cherimoya-0.0.1/cli/cherimoya +1355 -0
- cherimoya-0.0.1/setup.cfg +4 -0
- cherimoya-0.0.1/setup.py +27 -0
cherimoya-0.0.1/LICENSE
ADDED
|
@@ -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.
|
cherimoya-0.0.1/PKG-INFO
ADDED
|
@@ -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,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))
|