MaldiDeepKit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maldideepkit/__init__.py +54 -0
- maldideepkit/_bin_scaling.py +52 -0
- maldideepkit/_blocks.py +80 -0
- maldideepkit/attention/__init__.py +5 -0
- maldideepkit/attention/mlp.py +319 -0
- maldideepkit/augment/__init__.py +15 -0
- maldideepkit/augment/mixing.py +113 -0
- maldideepkit/augment/spectra.py +251 -0
- maldideepkit/base/__init__.py +15 -0
- maldideepkit/base/classifier.py +1079 -0
- maldideepkit/base/data.py +322 -0
- maldideepkit/blocks.py +39 -0
- maldideepkit/cnn/__init__.py +5 -0
- maldideepkit/cnn/cnn.py +316 -0
- maldideepkit/py.typed +0 -0
- maldideepkit/resnet/__init__.py +5 -0
- maldideepkit/resnet/resnet.py +380 -0
- maldideepkit/transformer/__init__.py +7 -0
- maldideepkit/transformer/transformer.py +492 -0
- maldideepkit/utils/__init__.py +22 -0
- maldideepkit/utils/calibration.py +134 -0
- maldideepkit/utils/ensemble.py +132 -0
- maldideepkit/utils/loss.py +138 -0
- maldideepkit/utils/lr_finder.py +173 -0
- maldideepkit/utils/reproducibility.py +70 -0
- maldideepkit/utils/sam.py +121 -0
- maldideepkit/utils/training.py +386 -0
- maldideepkit-0.1.0.dist-info/METADATA +301 -0
- maldideepkit-0.1.0.dist-info/RECORD +32 -0
- maldideepkit-0.1.0.dist-info/WHEEL +5 -0
- maldideepkit-0.1.0.dist-info/licenses/LICENSE +21 -0
- maldideepkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""1-D ResNet classifier for binned MALDI-TOF spectra.
|
|
2
|
+
|
|
3
|
+
ResNet-18 residual-block template adapted to 1-D spectral input: a
|
|
4
|
+
stem Conv1D, four stages of BasicBlock1D pairs with strided
|
|
5
|
+
downsampling between stages, global average pooling, and a single
|
|
6
|
+
linear head.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import torch
|
|
16
|
+
from torch import nn
|
|
17
|
+
|
|
18
|
+
from .._bin_scaling import scale_odd_kernel
|
|
19
|
+
from ..base.classifier import BaseSpectralClassifier
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BasicBlock1D(nn.Module):
|
|
23
|
+
"""Two Conv1D layers with a residual shortcut.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
in_channels : int
|
|
28
|
+
Number of input channels.
|
|
29
|
+
out_channels : int
|
|
30
|
+
Number of output channels.
|
|
31
|
+
stride : int, default=1
|
|
32
|
+
Stride of the first Conv1D. A stride ``!= 1`` (or a channel
|
|
33
|
+
mismatch) triggers a 1x1 projection on the shortcut path.
|
|
34
|
+
kernel_size : int, default=3
|
|
35
|
+
Kernel size of both Conv1D layers.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
expansion = 1
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
in_channels: int,
|
|
43
|
+
out_channels: int,
|
|
44
|
+
stride: int = 1,
|
|
45
|
+
kernel_size: int = 3,
|
|
46
|
+
) -> None:
|
|
47
|
+
super().__init__()
|
|
48
|
+
padding = kernel_size // 2
|
|
49
|
+
self.conv1 = nn.Conv1d(
|
|
50
|
+
in_channels,
|
|
51
|
+
out_channels,
|
|
52
|
+
kernel_size=kernel_size,
|
|
53
|
+
stride=stride,
|
|
54
|
+
padding=padding,
|
|
55
|
+
bias=False,
|
|
56
|
+
)
|
|
57
|
+
self.bn1 = nn.BatchNorm1d(out_channels)
|
|
58
|
+
self.conv2 = nn.Conv1d(
|
|
59
|
+
out_channels,
|
|
60
|
+
out_channels,
|
|
61
|
+
kernel_size=kernel_size,
|
|
62
|
+
stride=1,
|
|
63
|
+
padding=padding,
|
|
64
|
+
bias=False,
|
|
65
|
+
)
|
|
66
|
+
self.bn2 = nn.BatchNorm1d(out_channels)
|
|
67
|
+
self.relu = nn.ReLU(inplace=True)
|
|
68
|
+
|
|
69
|
+
if stride != 1 or in_channels != out_channels:
|
|
70
|
+
self.shortcut: nn.Module = nn.Sequential(
|
|
71
|
+
nn.Conv1d(
|
|
72
|
+
in_channels,
|
|
73
|
+
out_channels,
|
|
74
|
+
kernel_size=1,
|
|
75
|
+
stride=stride,
|
|
76
|
+
bias=False,
|
|
77
|
+
),
|
|
78
|
+
nn.BatchNorm1d(out_channels),
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
self.shortcut = nn.Identity()
|
|
82
|
+
|
|
83
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
84
|
+
"""Apply the residual block to a ``(N, C, L)`` activation tensor."""
|
|
85
|
+
identity = self.shortcut(x)
|
|
86
|
+
out = self.relu(self.bn1(self.conv1(x)))
|
|
87
|
+
out = self.bn2(self.conv2(out))
|
|
88
|
+
out = out + identity
|
|
89
|
+
return self.relu(out)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class SpectralResNet1D(nn.Module):
|
|
93
|
+
"""1-D ResNet-18 style backbone with a linear classification head.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
input_dim : int
|
|
98
|
+
Number of input bins.
|
|
99
|
+
n_classes : int, default=2
|
|
100
|
+
Number of output logits.
|
|
101
|
+
stem_channels : int, default=32
|
|
102
|
+
Output channels of the initial ``Conv1d`` + pool stem.
|
|
103
|
+
stage_channels : sequence of int, default=(64, 128, 256, 512)
|
|
104
|
+
Output channels of each stage. The first stage keeps the stem
|
|
105
|
+
resolution; later stages downsample by 2.
|
|
106
|
+
blocks_per_stage : sequence of int, default=(2, 2, 2, 2)
|
|
107
|
+
Number of BasicBlock1D pairs per stage.
|
|
108
|
+
stem_kernel_size : int, default=7
|
|
109
|
+
Kernel size of the stem Conv1D.
|
|
110
|
+
stem_stride : int, default=1
|
|
111
|
+
Stride of the stem Conv1D. Defaults to ``1`` (vs literal
|
|
112
|
+
ResNet-18's ``2``) to preserve peak-scale features through the
|
|
113
|
+
first stage on ~6000-bin spectra.
|
|
114
|
+
block_kernel_size : int, default=7
|
|
115
|
+
Kernel size inside every :class:`BasicBlock1D`. Widened from
|
|
116
|
+
ResNet-18's 3 to give each block enough local context for
|
|
117
|
+
peak-scale features.
|
|
118
|
+
use_stem_pool : bool, default=False
|
|
119
|
+
If ``True``, append the literal ResNet-18 ``MaxPool1d(kernel=3,
|
|
120
|
+
stride=2)`` after the stem Conv. Defaults to ``False`` because
|
|
121
|
+
the combined stem-Conv(stride=2) + MaxPool(stride=2) initial
|
|
122
|
+
4x downsampling collapses peak-scale features. Set to ``True``
|
|
123
|
+
to reproduce the literal ResNet-18 backbone.
|
|
124
|
+
dropout : float, default=0.2
|
|
125
|
+
Dropout applied after global average pooling.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
input_dim: int,
|
|
131
|
+
n_classes: int = 2,
|
|
132
|
+
stem_channels: int = 32,
|
|
133
|
+
stage_channels: tuple[int, ...] = (64, 128, 256, 512),
|
|
134
|
+
blocks_per_stage: tuple[int, ...] = (2, 2, 2, 2),
|
|
135
|
+
stem_kernel_size: int = 7,
|
|
136
|
+
stem_stride: int = 1,
|
|
137
|
+
block_kernel_size: int = 7,
|
|
138
|
+
use_stem_pool: bool = False,
|
|
139
|
+
dropout: float = 0.2,
|
|
140
|
+
) -> None:
|
|
141
|
+
super().__init__()
|
|
142
|
+
if len(stage_channels) != len(blocks_per_stage):
|
|
143
|
+
raise ValueError(
|
|
144
|
+
"stage_channels and blocks_per_stage must have the same length."
|
|
145
|
+
)
|
|
146
|
+
stem_layers: list[nn.Module] = [
|
|
147
|
+
nn.Conv1d(
|
|
148
|
+
1,
|
|
149
|
+
stem_channels,
|
|
150
|
+
kernel_size=stem_kernel_size,
|
|
151
|
+
stride=stem_stride,
|
|
152
|
+
padding=stem_kernel_size // 2,
|
|
153
|
+
bias=False,
|
|
154
|
+
),
|
|
155
|
+
nn.BatchNorm1d(stem_channels),
|
|
156
|
+
nn.ReLU(inplace=True),
|
|
157
|
+
]
|
|
158
|
+
if use_stem_pool:
|
|
159
|
+
stem_layers.append(nn.MaxPool1d(kernel_size=3, stride=2, padding=1))
|
|
160
|
+
self.stem = nn.Sequential(*stem_layers)
|
|
161
|
+
|
|
162
|
+
stages: list[nn.Module] = []
|
|
163
|
+
prev = stem_channels
|
|
164
|
+
for i, (ch, n_blocks) in enumerate(
|
|
165
|
+
zip(stage_channels, blocks_per_stage, strict=True)
|
|
166
|
+
):
|
|
167
|
+
stride = 1 if i == 0 else 2
|
|
168
|
+
layer = [
|
|
169
|
+
BasicBlock1D(prev, ch, stride=stride, kernel_size=block_kernel_size)
|
|
170
|
+
]
|
|
171
|
+
layer += [
|
|
172
|
+
BasicBlock1D(ch, ch, stride=1, kernel_size=block_kernel_size)
|
|
173
|
+
for _ in range(n_blocks - 1)
|
|
174
|
+
]
|
|
175
|
+
stages.append(nn.Sequential(*layer))
|
|
176
|
+
prev = ch
|
|
177
|
+
self.stages = nn.Sequential(*stages)
|
|
178
|
+
|
|
179
|
+
self.gap = nn.AdaptiveAvgPool1d(1)
|
|
180
|
+
self.dropout = nn.Dropout(dropout)
|
|
181
|
+
self.fc = nn.Linear(prev, n_classes)
|
|
182
|
+
self.input_dim = input_dim
|
|
183
|
+
|
|
184
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
185
|
+
"""Map ``(batch, input_dim)`` to ``(batch, n_classes)`` logits."""
|
|
186
|
+
x = x.unsqueeze(1)
|
|
187
|
+
x = self.stem(x)
|
|
188
|
+
x = self.stages(x)
|
|
189
|
+
x = self.gap(x).squeeze(-1)
|
|
190
|
+
x = self.dropout(x)
|
|
191
|
+
return self.fc(x)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class MaldiResNetClassifier(BaseSpectralClassifier):
|
|
195
|
+
"""sklearn-compatible 1-D ResNet classifier for MALDI-TOF spectra.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
stem_channels : int, default=32
|
|
200
|
+
Output channels of the stem.
|
|
201
|
+
stage_channels : sequence of int, default=(64, 128, 256, 512)
|
|
202
|
+
Output channels of each residual stage.
|
|
203
|
+
blocks_per_stage : sequence of int, default=(2, 2, 2, 2)
|
|
204
|
+
Number of residual blocks per stage (ResNet-18 topology).
|
|
205
|
+
stem_kernel_size : int, default=7
|
|
206
|
+
Kernel size of the stem Conv1D. Calibrated for ``bin_width=3``;
|
|
207
|
+
:meth:`from_spectrum` scales it for other bin widths.
|
|
208
|
+
stem_stride : int, default=1
|
|
209
|
+
Stride of the stem Conv1D. Defaults to ``1`` so the first
|
|
210
|
+
residual stage sees full-resolution input.
|
|
211
|
+
block_kernel_size : int, default=7
|
|
212
|
+
Kernel size inside every :class:`BasicBlock1D`. Widened from
|
|
213
|
+
ResNet-18's 3 so each block has enough local context for
|
|
214
|
+
peak-scale features.
|
|
215
|
+
use_stem_pool : bool, default=False
|
|
216
|
+
If ``True``, append the literal ResNet-18 MaxPool after the
|
|
217
|
+
stem Conv. Defaults to ``False`` because the combined
|
|
218
|
+
stem(stride=2) + MaxPool(stride=2) 4x downsampling is too
|
|
219
|
+
aggressive for MALDI-TOF peak structure. Set to ``True`` to
|
|
220
|
+
reproduce the literal ResNet-18 backbone.
|
|
221
|
+
dropout : float, default=0.2
|
|
222
|
+
Dropout before the final linear layer.
|
|
223
|
+
**kwargs
|
|
224
|
+
Forwarded to :class:`~maldideepkit.base.classifier.BaseSpectralClassifier`.
|
|
225
|
+
|
|
226
|
+
Notes
|
|
227
|
+
-----
|
|
228
|
+
Defaults deviate from literal ResNet-18 (He et al., 2016) in three
|
|
229
|
+
ways for MALDI-TOF: ``stem_stride=1`` (was 2),
|
|
230
|
+
``block_kernel_size=7`` (was 3), and ``use_stem_pool=False`` (was
|
|
231
|
+
True). The literal backbone is reproducible via ``stem_stride=2,
|
|
232
|
+
block_kernel_size=3, use_stem_pool=True``.
|
|
233
|
+
|
|
234
|
+
The final head is a ``Linear(stage_channels[-1], n_classes)``
|
|
235
|
+
after global average pooling, so the parameter count is
|
|
236
|
+
**independent of ``input_dim``**.
|
|
237
|
+
|
|
238
|
+
Examples
|
|
239
|
+
--------
|
|
240
|
+
>>> import numpy as np
|
|
241
|
+
>>> from maldideepkit import MaldiResNetClassifier
|
|
242
|
+
>>> rng = np.random.default_rng(0)
|
|
243
|
+
>>> X = rng.standard_normal((32, 512)).astype("float32")
|
|
244
|
+
>>> y = rng.integers(0, 2, size=32)
|
|
245
|
+
>>> clf = MaldiResNetClassifier(
|
|
246
|
+
... epochs=2, batch_size=8, stage_channels=(16, 32),
|
|
247
|
+
... blocks_per_stage=(1, 1), random_state=0
|
|
248
|
+
... ).fit(X, y)
|
|
249
|
+
>>> clf.predict(X).shape
|
|
250
|
+
(32,)
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def __init__(
|
|
254
|
+
self,
|
|
255
|
+
input_dim: int | None = None,
|
|
256
|
+
n_classes: int = 2,
|
|
257
|
+
stem_channels: int = 32,
|
|
258
|
+
stage_channels: tuple[int, ...] = (64, 128, 256, 512),
|
|
259
|
+
blocks_per_stage: tuple[int, ...] = (2, 2, 2, 2),
|
|
260
|
+
stem_kernel_size: int = 7,
|
|
261
|
+
stem_stride: int = 1,
|
|
262
|
+
block_kernel_size: int = 7,
|
|
263
|
+
use_stem_pool: bool = False,
|
|
264
|
+
dropout: float = 0.2,
|
|
265
|
+
learning_rate: float = 1e-3,
|
|
266
|
+
weight_decay: float = 1e-4,
|
|
267
|
+
grad_clip_norm: float | None = 1.0,
|
|
268
|
+
label_smoothing: float = 0.0,
|
|
269
|
+
loss: str = "cross_entropy",
|
|
270
|
+
focal_gamma: float = 2.0,
|
|
271
|
+
use_amp: bool = False,
|
|
272
|
+
swa_start_epoch: int | None = None,
|
|
273
|
+
tune_threshold: bool = False,
|
|
274
|
+
threshold_metric: str = "balanced_accuracy",
|
|
275
|
+
calibrate_temperature: bool = False,
|
|
276
|
+
min_val_auroc_for_threshold_tune: float = 0.6,
|
|
277
|
+
use_sam: bool = False,
|
|
278
|
+
sam_rho: float = 0.05,
|
|
279
|
+
batch_size: int = 32,
|
|
280
|
+
epochs: int = 100,
|
|
281
|
+
early_stopping_patience: int = 10,
|
|
282
|
+
val_fraction: float = 0.1,
|
|
283
|
+
warmup_epochs: int = 5,
|
|
284
|
+
standardize: bool = False,
|
|
285
|
+
input_transform: str | None = "log1p",
|
|
286
|
+
warping: Any | None = None,
|
|
287
|
+
metrics_log_path: str | Path | None = None,
|
|
288
|
+
track_train_metrics: bool = False,
|
|
289
|
+
augment: Any | None = None,
|
|
290
|
+
mixup_alpha: float = 0.0,
|
|
291
|
+
cutmix_alpha: float = 0.0,
|
|
292
|
+
ema_decay: float | None = None,
|
|
293
|
+
retry_on_val_auroc_below: float | None = None,
|
|
294
|
+
max_retries: int = 2,
|
|
295
|
+
class_weight: str | np.ndarray | list | None = None,
|
|
296
|
+
device: str | torch.device = "auto",
|
|
297
|
+
random_state: int = 0,
|
|
298
|
+
verbose: bool = False,
|
|
299
|
+
) -> None:
|
|
300
|
+
super().__init__(
|
|
301
|
+
input_dim=input_dim,
|
|
302
|
+
n_classes=n_classes,
|
|
303
|
+
learning_rate=learning_rate,
|
|
304
|
+
weight_decay=weight_decay,
|
|
305
|
+
grad_clip_norm=grad_clip_norm,
|
|
306
|
+
label_smoothing=label_smoothing,
|
|
307
|
+
loss=loss,
|
|
308
|
+
focal_gamma=focal_gamma,
|
|
309
|
+
use_amp=use_amp,
|
|
310
|
+
swa_start_epoch=swa_start_epoch,
|
|
311
|
+
tune_threshold=tune_threshold,
|
|
312
|
+
threshold_metric=threshold_metric,
|
|
313
|
+
calibrate_temperature=calibrate_temperature,
|
|
314
|
+
min_val_auroc_for_threshold_tune=min_val_auroc_for_threshold_tune,
|
|
315
|
+
use_sam=use_sam,
|
|
316
|
+
sam_rho=sam_rho,
|
|
317
|
+
batch_size=batch_size,
|
|
318
|
+
epochs=epochs,
|
|
319
|
+
early_stopping_patience=early_stopping_patience,
|
|
320
|
+
val_fraction=val_fraction,
|
|
321
|
+
warmup_epochs=warmup_epochs,
|
|
322
|
+
standardize=standardize,
|
|
323
|
+
input_transform=input_transform,
|
|
324
|
+
warping=warping,
|
|
325
|
+
metrics_log_path=metrics_log_path,
|
|
326
|
+
track_train_metrics=track_train_metrics,
|
|
327
|
+
augment=augment,
|
|
328
|
+
mixup_alpha=mixup_alpha,
|
|
329
|
+
cutmix_alpha=cutmix_alpha,
|
|
330
|
+
ema_decay=ema_decay,
|
|
331
|
+
retry_on_val_auroc_below=retry_on_val_auroc_below,
|
|
332
|
+
max_retries=max_retries,
|
|
333
|
+
class_weight=class_weight,
|
|
334
|
+
device=device,
|
|
335
|
+
random_state=random_state,
|
|
336
|
+
verbose=verbose,
|
|
337
|
+
)
|
|
338
|
+
self.stem_channels = stem_channels
|
|
339
|
+
self.stage_channels = stage_channels
|
|
340
|
+
self.blocks_per_stage = blocks_per_stage
|
|
341
|
+
self.stem_kernel_size = stem_kernel_size
|
|
342
|
+
self.stem_stride = stem_stride
|
|
343
|
+
self.block_kernel_size = block_kernel_size
|
|
344
|
+
self.use_stem_pool = use_stem_pool
|
|
345
|
+
self.dropout = dropout
|
|
346
|
+
|
|
347
|
+
def _build_model(self) -> nn.Module:
|
|
348
|
+
return SpectralResNet1D(
|
|
349
|
+
input_dim=self.input_dim_,
|
|
350
|
+
n_classes=self.n_classes_,
|
|
351
|
+
stem_channels=int(self.stem_channels),
|
|
352
|
+
stage_channels=tuple(self.stage_channels),
|
|
353
|
+
blocks_per_stage=tuple(self.blocks_per_stage),
|
|
354
|
+
stem_kernel_size=int(self.stem_kernel_size),
|
|
355
|
+
stem_stride=int(self.stem_stride),
|
|
356
|
+
block_kernel_size=int(self.block_kernel_size),
|
|
357
|
+
use_stem_pool=bool(self.use_stem_pool),
|
|
358
|
+
dropout=float(self.dropout),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
@classmethod
|
|
362
|
+
def from_spectrum(
|
|
363
|
+
cls, bin_width: int, input_dim: int, **overrides
|
|
364
|
+
) -> "MaldiResNetClassifier":
|
|
365
|
+
"""Construct a peak-friendly ResNet for a given spectrum layout.
|
|
366
|
+
|
|
367
|
+
Scales ``stem_kernel_size`` inversely with ``bin_width``
|
|
368
|
+
relative to the package reference (``bin_width=3``,
|
|
369
|
+
``stem_kernel_size=7``). Also sets ``stem_stride=1`` and
|
|
370
|
+
``use_stem_pool=False`` (the class defaults). Any keyword in
|
|
371
|
+
``**overrides`` wins over the auto-selected values.
|
|
372
|
+
"""
|
|
373
|
+
kwargs: dict[str, Any] = {
|
|
374
|
+
"input_dim": input_dim,
|
|
375
|
+
"stem_kernel_size": scale_odd_kernel(bin_width),
|
|
376
|
+
"stem_stride": 1,
|
|
377
|
+
"use_stem_pool": False,
|
|
378
|
+
}
|
|
379
|
+
kwargs.update(overrides)
|
|
380
|
+
return cls(**kwargs)
|