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.
@@ -0,0 +1,322 @@
1
+ """Dataset and DataLoader helpers for MALDI-TOF spectra."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ import torch
10
+ from sklearn.model_selection import train_test_split
11
+ from torch.utils.data import DataLoader, Dataset
12
+
13
+ _STD_FLOOR = 1e-7
14
+
15
+
16
+ def _to_numpy(X: Any) -> np.ndarray:
17
+ """Return ``X`` as a float32 2-D ndarray.
18
+
19
+ Accepts NumPy arrays, pandas DataFrames/Series, and any object
20
+ exposing a DataFrame-like ``.X`` attribute (e.g. :class:`maldiamrkit.MaldiSet`).
21
+ """
22
+ if hasattr(X, "X") and not isinstance(X, np.ndarray):
23
+ X = X.X
24
+ if hasattr(X, "to_numpy"):
25
+ X = X.to_numpy()
26
+ X = np.asarray(X, dtype=np.float32)
27
+ if X.ndim == 1:
28
+ X = X.reshape(1, -1)
29
+ if X.ndim != 2:
30
+ raise ValueError(f"Expected 2-D feature matrix, got shape {X.shape} instead.")
31
+ return X
32
+
33
+
34
+ class SpectralDataset(Dataset):
35
+ """PyTorch ``Dataset`` wrapping a binned MALDI-TOF feature matrix.
36
+
37
+ The dataset stores its spectra as a single float32 tensor in memory
38
+ and optionally standardizes each feature on the fly using
39
+ statistics computed once at construction time.
40
+
41
+ Parameters
42
+ ----------
43
+ X : array-like or MaldiSet
44
+ Feature matrix of shape ``(n_samples, n_bins)``. A NumPy array,
45
+ a pandas DataFrame, or any object with a DataFrame-like ``.X``
46
+ attribute is accepted.
47
+ y : array-like, optional
48
+ Integer class labels of shape ``(n_samples,)``. When ``None``
49
+ (inference usage) the dataset yields only features.
50
+ standardize : bool, default=False
51
+ If ``True``, subtract the per-column mean and divide by the
52
+ per-column standard deviation computed from ``X``. Columns with
53
+ zero variance are left untouched.
54
+ mean : array-like, optional
55
+ Pre-computed per-feature means. Used together with ``std`` to
56
+ apply an external standardization (e.g. one fitted on a training
57
+ fold). Ignored when ``standardize=False``.
58
+ std : array-like, optional
59
+ Pre-computed per-feature standard deviations. Ignored when
60
+ ``standardize=False``.
61
+
62
+ Attributes
63
+ ----------
64
+ X : torch.Tensor
65
+ Stored features as a float32 tensor.
66
+ y : torch.Tensor or None
67
+ Stored labels as a long tensor, or ``None`` for inference.
68
+ mean : torch.Tensor or None
69
+ Feature-wise mean used for standardization.
70
+ std : torch.Tensor or None
71
+ Feature-wise standard deviation used for standardization.
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ X: Any,
77
+ y: Any | None = None,
78
+ *,
79
+ standardize: bool = False,
80
+ mean: np.ndarray | None = None,
81
+ std: np.ndarray | None = None,
82
+ ) -> None:
83
+ X_np = _to_numpy(X)
84
+
85
+ if standardize:
86
+ if mean is None or std is None:
87
+ mean = X_np.mean(axis=0)
88
+ std = X_np.std(axis=0)
89
+ std = np.asarray(std, dtype=np.float32)
90
+ mean = np.asarray(mean, dtype=np.float32)
91
+ safe_std = np.maximum(std, _STD_FLOOR).astype(np.float32)
92
+ X_np = (X_np - mean) / safe_std
93
+ self.mean: torch.Tensor | None = torch.from_numpy(
94
+ np.asarray(mean, dtype=np.float32)
95
+ )
96
+ self.std: torch.Tensor | None = torch.from_numpy(safe_std)
97
+ else:
98
+ self.mean = None
99
+ self.std = None
100
+
101
+ self.X = torch.from_numpy(X_np)
102
+
103
+ if y is not None:
104
+ if hasattr(y, "to_numpy"):
105
+ y = y.to_numpy()
106
+ y_np = np.asarray(y).ravel()
107
+ self.y: torch.Tensor | None = torch.from_numpy(y_np.astype(np.int64))
108
+ else:
109
+ self.y = None
110
+
111
+ def __len__(self) -> int:
112
+ return int(self.X.shape[0])
113
+
114
+ def __getitem__(self, idx: int) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
115
+ if self.y is None:
116
+ return self.X[idx]
117
+ return self.X[idx], self.y[idx]
118
+
119
+
120
+ INPUT_TRANSFORMS = ("standardize", "log1p", "robust", "log1p+standardize", "none")
121
+
122
+
123
+ def fit_input_transform(X_tr: np.ndarray, mode: str) -> dict[str, Any]:
124
+ """Compute per-bin statistics for the requested input-transform mode.
125
+
126
+ Fitted on the training split only to keep the pipeline leak-safe.
127
+ Returns a state dict that :func:`apply_input_transform` consumes
128
+ at training and inference time.
129
+
130
+ Supported modes:
131
+
132
+ - ``"none"``: identity; empty state.
133
+ - ``"standardize"``: per-bin ``(X - mean) / std`` from the train split.
134
+ - ``"log1p"``: element-wise ``log1p(clip(X, 0, None))``; stateless.
135
+ - ``"robust"``: per-bin ``(X - median) / IQR`` from the train split.
136
+ A zero IQR bin is treated as unit scale.
137
+ - ``"log1p+standardize"``: ``log1p`` first, then standardize.
138
+ """
139
+ if mode not in INPUT_TRANSFORMS:
140
+ raise ValueError(
141
+ f"Unknown input_transform={mode!r}; expected one of {INPUT_TRANSFORMS}."
142
+ )
143
+ state: dict[str, Any] = {"mode": mode}
144
+ if mode == "log1p+standardize":
145
+ X_fit = np.log1p(np.clip(X_tr, 0, None))
146
+ else:
147
+ X_fit = X_tr
148
+
149
+ if mode in {"standardize", "log1p+standardize"}:
150
+ state["mean"] = X_fit.mean(axis=0).astype(np.float32)
151
+ state["std"] = X_fit.std(axis=0).astype(np.float32)
152
+ elif mode == "robust":
153
+ state["median"] = np.median(X_fit, axis=0).astype(np.float32)
154
+ q75, q25 = np.percentile(X_fit, [75, 25], axis=0)
155
+ iqr = (q75 - q25).astype(np.float32)
156
+ state["iqr"] = np.maximum(iqr, _STD_FLOOR).astype(np.float32)
157
+ return state
158
+
159
+
160
+ def apply_input_transform(X: np.ndarray, state: dict[str, Any]) -> np.ndarray:
161
+ """Apply a fitted :func:`fit_input_transform` state to ``X``."""
162
+ mode = state.get("mode", "none")
163
+ if mode == "none":
164
+ return X.astype(np.float32, copy=False)
165
+ if mode == "log1p":
166
+ return np.log1p(np.clip(X, 0, None)).astype(np.float32, copy=False)
167
+ if mode == "standardize":
168
+ mean = np.asarray(state["mean"], dtype=np.float32)
169
+ std = np.asarray(state["std"], dtype=np.float32)
170
+ safe_std = np.maximum(std, _STD_FLOOR).astype(np.float32)
171
+ return ((X - mean) / safe_std).astype(np.float32, copy=False)
172
+ if mode == "log1p+standardize":
173
+ logged = np.log1p(np.clip(X, 0, None))
174
+ mean = np.asarray(state["mean"], dtype=np.float32)
175
+ std = np.asarray(state["std"], dtype=np.float32)
176
+ safe_std = np.maximum(std, _STD_FLOOR).astype(np.float32)
177
+ return ((logged - mean) / safe_std).astype(np.float32, copy=False)
178
+ if mode == "robust":
179
+ median = np.asarray(state["median"], dtype=np.float32)
180
+ iqr = np.asarray(state["iqr"], dtype=np.float32)
181
+ return ((X - median) / iqr).astype(np.float32, copy=False)
182
+ raise ValueError(f"Unknown input_transform state mode={mode!r}.")
183
+
184
+
185
+ def _warp_numpy(warper: Any, X_np: np.ndarray) -> np.ndarray:
186
+ """Apply a fitted warper to a numpy matrix, returning a numpy matrix."""
187
+ df = pd.DataFrame(X_np)
188
+ out = warper.transform(df)
189
+ if hasattr(out, "to_numpy"):
190
+ out = out.to_numpy()
191
+ return np.asarray(out, dtype=np.float32)
192
+
193
+
194
+ def make_loaders(
195
+ X: Any,
196
+ y: Any,
197
+ *,
198
+ batch_size: int = 32,
199
+ val_size: float = 0.1,
200
+ random_state: int | None = 0,
201
+ standardize: bool = False,
202
+ input_transform: str | None = None,
203
+ stratify: bool = True,
204
+ num_workers: int = 0,
205
+ warper: Any | None = None,
206
+ ) -> tuple[DataLoader, DataLoader, dict[str, Any]]:
207
+ """Build stratified train / validation :class:`DataLoader` pairs.
208
+
209
+ Pipeline order, applied **after** the train/val split so nothing
210
+ from the validation split leaks into training statistics:
211
+
212
+ 1. Spectral warping / alignment (if ``warper`` is given): fit on
213
+ the training split, then transform both splits.
214
+ 2. Per-feature standardization (if ``standardize=True``): fit
215
+ mean/std on the (warped) training split, then apply to both
216
+ splits.
217
+
218
+ Parameters
219
+ ----------
220
+ X : array-like or MaldiSet
221
+ Feature matrix of shape ``(n_samples, n_bins)``.
222
+ y : array-like
223
+ Integer class labels of shape ``(n_samples,)``.
224
+ batch_size : int, default=32
225
+ Mini-batch size for the training loader.
226
+ val_size : float, default=0.1
227
+ Fraction of the input held out for validation.
228
+ random_state : int or None, default=0
229
+ Seed for the split.
230
+ standardize : bool, default=False
231
+ Shorthand for ``input_transform="standardize"`` (when True) or
232
+ ``input_transform="none"`` (when False). Kept for backwards
233
+ compatibility; the modern interface is ``input_transform``.
234
+ Ignored whenever ``input_transform`` is given explicitly.
235
+ input_transform : str, optional
236
+ One of ``{"none", "standardize", "log1p", "robust",
237
+ "log1p+standardize"}``. Fitted on the (warped) training split
238
+ only and applied to both splits. Overrides ``standardize``
239
+ when both are given.
240
+ stratify : bool, default=True
241
+ If ``True`` and all classes have at least two samples, stratify
242
+ the split on ``y``. Falls back to random split otherwise.
243
+ num_workers : int, default=0
244
+ ``DataLoader`` worker count.
245
+ warper : sklearn-style transformer, optional
246
+ Unfitted spectral-alignment transformer with ``fit(X) ->
247
+ self`` + ``transform(X) -> X``. Fitted on the training split
248
+ only and used to transform both splits. The fitted object is
249
+ returned in ``stats["warper"]``.
250
+
251
+ Returns
252
+ -------
253
+ train_loader : DataLoader
254
+ Shuffling training loader. Drops the last batch when it would
255
+ contain a single sample (avoids ``BatchNorm`` issues).
256
+ val_loader : DataLoader
257
+ Non-shuffling validation loader.
258
+ stats : dict
259
+ ``{"mean": array or None, "std": array or None, "warper":
260
+ fitted warper or None, "input_transform_state": dict}``.
261
+ """
262
+ X_np = _to_numpy(X)
263
+ if hasattr(y, "to_numpy"):
264
+ y = y.to_numpy()
265
+ y_np = np.asarray(y).ravel()
266
+
267
+ _, counts = np.unique(y_np, return_counts=True)
268
+ can_stratify = stratify and counts.min() >= 2
269
+
270
+ X_tr, X_val, y_tr, y_val = train_test_split(
271
+ X_np,
272
+ y_np,
273
+ test_size=val_size,
274
+ random_state=random_state,
275
+ stratify=y_np if can_stratify else None,
276
+ )
277
+
278
+ fitted_warper = None
279
+ if warper is not None:
280
+ fitted_warper = warper.fit(pd.DataFrame(X_tr))
281
+ X_tr = _warp_numpy(fitted_warper, X_tr)
282
+ X_val = _warp_numpy(fitted_warper, X_val)
283
+
284
+ if input_transform is None:
285
+ transform_mode = "standardize" if standardize else "none"
286
+ else:
287
+ transform_mode = input_transform
288
+ transform_state = fit_input_transform(X_tr, transform_mode)
289
+ X_tr = apply_input_transform(X_tr, transform_state)
290
+ X_val = apply_input_transform(X_val, transform_state)
291
+
292
+ mean = transform_state.get("mean")
293
+ std = transform_state.get("std")
294
+
295
+ train_ds = SpectralDataset(X_tr, y_tr, standardize=False)
296
+ val_ds = SpectralDataset(X_val, y_val, standardize=False)
297
+
298
+ drop_last = batch_size > 1 and (len(train_ds) % batch_size) == 1
299
+ train_loader = DataLoader(
300
+ train_ds,
301
+ batch_size=batch_size,
302
+ shuffle=True,
303
+ drop_last=drop_last,
304
+ num_workers=num_workers,
305
+ )
306
+ val_loader = DataLoader(
307
+ val_ds,
308
+ batch_size=batch_size,
309
+ shuffle=False,
310
+ drop_last=False,
311
+ num_workers=num_workers,
312
+ )
313
+ return (
314
+ train_loader,
315
+ val_loader,
316
+ {
317
+ "mean": mean,
318
+ "std": std,
319
+ "warper": fitted_warper,
320
+ "input_transform_state": transform_state,
321
+ },
322
+ )
maldideepkit/blocks.py ADDED
@@ -0,0 +1,39 @@
1
+ """One-stop import path for every MaldiDeepKit ``nn.Module`` primitive.
2
+
3
+ Re-exports the full backbones and composable primitives under a single
4
+ namespace so users embedding components into their own networks don't
5
+ have to know the per-family layout.
6
+
7
+ Examples
8
+ --------
9
+ >>> import torch
10
+ >>> from maldideepkit.blocks import SpectralTransformer1D, TransformerBlock
11
+ >>> backbone = SpectralTransformer1D(input_dim=6000, depth=6)
12
+ >>> block = TransformerBlock(dim=128, num_heads=4)
13
+ >>> tokens = torch.randn(2, 1500, 128)
14
+ >>> out = block(tokens) # (2, 1500, 128)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from ._blocks import DropPath, PatchEmbed1D
20
+ from .attention.mlp import SpectralAttentionMLP
21
+ from .cnn.cnn import SpectralCNN1D
22
+ from .resnet.resnet import BasicBlock1D, SpectralResNet1D
23
+ from .transformer.transformer import (
24
+ MultiHeadSelfAttention,
25
+ SpectralTransformer1D,
26
+ TransformerBlock,
27
+ )
28
+
29
+ __all__ = [
30
+ "SpectralAttentionMLP",
31
+ "SpectralCNN1D",
32
+ "SpectralResNet1D",
33
+ "BasicBlock1D",
34
+ "SpectralTransformer1D",
35
+ "TransformerBlock",
36
+ "MultiHeadSelfAttention",
37
+ "PatchEmbed1D",
38
+ "DropPath",
39
+ ]
@@ -0,0 +1,5 @@
1
+ """1-D convolutional classifier for MALDI-TOF spectra."""
2
+
3
+ from .cnn import MaldiCNNClassifier, SpectralCNN1D
4
+
5
+ __all__ = ["MaldiCNNClassifier", "SpectralCNN1D"]
@@ -0,0 +1,316 @@
1
+ """1-D convolutional classifier for binned MALDI-TOF spectra.
2
+
3
+ A stack of ``Conv1d -> BatchNorm -> ReLU -> MaxPool`` blocks followed
4
+ by a flatten + dense classification head.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Any, Sequence
11
+
12
+ import numpy as np
13
+ import torch
14
+ from torch import nn
15
+
16
+ from .._bin_scaling import scale_odd_kernel
17
+ from ..base.classifier import BaseSpectralClassifier
18
+
19
+
20
+ def _broadcast(value: int | Sequence[int], n: int, name: str) -> tuple[int, ...]:
21
+ """Return a length-``n`` tuple: scalars are broadcast, sequences validated."""
22
+ if isinstance(value, int):
23
+ if value <= 0:
24
+ raise ValueError(f"{name} must be a positive integer; got {value}.")
25
+ return (value,) * n
26
+ out = tuple(int(v) for v in value)
27
+ if len(out) != n:
28
+ raise ValueError(
29
+ f"{name} has length {len(out)} but must have length {n} to match channels."
30
+ )
31
+ if any(v <= 0 for v in out):
32
+ raise ValueError(f"{name} must contain only positive integers; got {out}.")
33
+ return out
34
+
35
+
36
+ class _ConvBlock(nn.Module):
37
+ """One Conv1D + BN + ReLU + MaxPool + Dropout block."""
38
+
39
+ def __init__(
40
+ self,
41
+ in_channels: int,
42
+ out_channels: int,
43
+ kernel_size: int,
44
+ pool_size: int,
45
+ dropout: float,
46
+ ) -> None:
47
+ super().__init__()
48
+ padding = kernel_size // 2
49
+ self.block = nn.Sequential(
50
+ nn.Conv1d(in_channels, out_channels, kernel_size, padding=padding),
51
+ nn.BatchNorm1d(out_channels),
52
+ nn.ReLU(inplace=True),
53
+ nn.MaxPool1d(pool_size),
54
+ nn.Dropout(dropout),
55
+ )
56
+
57
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
58
+ return self.block(x)
59
+
60
+
61
+ class SpectralCNN1D(nn.Module):
62
+ """Stack of Conv1D blocks with a dense classification head.
63
+
64
+ Parameters
65
+ ----------
66
+ input_dim : int
67
+ Number of input bins.
68
+ n_classes : int, default=2
69
+ Number of output logits.
70
+ channels : sequence of int, default=(32, 64, 128, 128)
71
+ Output channels of each convolutional block.
72
+ kernel_size : int or sequence of int, default=7
73
+ Kernel size per block. A scalar is broadcast to every block;
74
+ a sequence must have the same length as ``channels``.
75
+ pool_size : int or sequence of int, default=2
76
+ Pool factor per block. A scalar is broadcast to every block;
77
+ a sequence must have the same length as ``channels``.
78
+ head_dim : int, default=128
79
+ Width of the single hidden dense layer.
80
+ dropout : float, default=0.3
81
+ Dropout applied inside every block and before the output layer.
82
+
83
+ Notes
84
+ -----
85
+ Input tensors have shape ``(batch, input_dim)`` and are unsqueezed to
86
+ ``(batch, 1, input_dim)`` internally.
87
+ """
88
+
89
+ def __init__(
90
+ self,
91
+ input_dim: int,
92
+ n_classes: int = 2,
93
+ channels: tuple[int, ...] = (32, 64, 128, 128),
94
+ kernel_size: int | Sequence[int] = 7,
95
+ pool_size: int | Sequence[int] = 2,
96
+ head_dim: int = 128,
97
+ dropout: float = 0.3,
98
+ ) -> None:
99
+ super().__init__()
100
+ n_blocks = len(channels)
101
+ kernels = _broadcast(kernel_size, n_blocks, "kernel_size")
102
+ pools = _broadcast(pool_size, n_blocks, "pool_size")
103
+
104
+ blocks: list[nn.Module] = []
105
+ prev = 1
106
+ length = input_dim
107
+ for out_ch, k, p in zip(channels, kernels, pools, strict=True):
108
+ blocks.append(_ConvBlock(prev, out_ch, k, p, dropout))
109
+ prev = out_ch
110
+ length //= p
111
+ if length <= 0:
112
+ raise ValueError(
113
+ f"input_dim={input_dim} is too small for the given pool "
114
+ f"schedule {pools} (block {len(blocks)} would have 0 length)."
115
+ )
116
+ self.backbone = nn.Sequential(*blocks)
117
+ self.flat_dim = prev * length
118
+ self.head = nn.Sequential(
119
+ nn.Linear(self.flat_dim, head_dim),
120
+ nn.LayerNorm(head_dim),
121
+ nn.ReLU(inplace=True),
122
+ nn.Dropout(dropout),
123
+ nn.Linear(head_dim, n_classes),
124
+ )
125
+
126
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
127
+ """Map ``(batch, input_dim)`` to ``(batch, n_classes)`` logits."""
128
+ x = x.unsqueeze(1)
129
+ feat = self.backbone(x)
130
+ return self.head(feat.flatten(1))
131
+
132
+
133
+ class MaldiCNNClassifier(BaseSpectralClassifier):
134
+ """sklearn-compatible 1-D CNN classifier for MALDI-TOF spectra.
135
+
136
+ Parameters
137
+ ----------
138
+ channels : sequence of int, default=(32, 64, 128, 128)
139
+ Output channels of each convolutional block. The effective spatial
140
+ resolution is divided by the corresponding ``pool_size`` after
141
+ every block.
142
+ kernel_size : int or sequence of int, default=7
143
+ Kernel size per block. A scalar is broadcast; a sequence must
144
+ match the length of ``channels``. The default is calibrated for
145
+ ``bin_width=3``; :meth:`from_spectrum` scales it for other bin
146
+ widths.
147
+ pool_size : int or sequence of int, default=2
148
+ Pool factor per block. Accepts scalar or per-block sequence.
149
+ head_dim : int, default=128
150
+ Width of the hidden dense layer.
151
+ dropout : float, default=0.3
152
+ Dropout applied inside every block and before the output layer.
153
+ **kwargs
154
+ Forwarded to :class:`~maldideepkit.base.classifier.BaseSpectralClassifier`.
155
+
156
+ Notes
157
+ -----
158
+ The flat dense head scales linearly with ``input_dim``; prefer
159
+ :class:`~maldideepkit.MaldiResNetClassifier` or
160
+ :class:`~maldideepkit.MaldiTransformerClassifier` if you want
161
+ a head width that's independent of the input resolution.
162
+
163
+ See :func:`from_spectrum` for a factory that auto-scales
164
+ ``kernel_size`` for a given ``(bin_width, input_dim)`` layout.
165
+
166
+ Examples
167
+ --------
168
+ >>> import numpy as np
169
+ >>> from maldideepkit import MaldiCNNClassifier
170
+ >>> rng = np.random.default_rng(0)
171
+ >>> X = rng.standard_normal((32, 256)).astype("float32")
172
+ >>> y = rng.integers(0, 2, size=32)
173
+ >>> clf = MaldiCNNClassifier(epochs=2, batch_size=8, random_state=0).fit(X, y)
174
+ >>> clf.predict(X).shape
175
+ (32,)
176
+
177
+ Per-block kernel progression:
178
+
179
+ >>> clf = MaldiCNNClassifier(
180
+ ... channels=(32, 64, 128, 128),
181
+ ... kernel_size=(11, 7, 5, 3),
182
+ ... )
183
+ """
184
+
185
+ def __init__(
186
+ self,
187
+ input_dim: int | None = None,
188
+ n_classes: int = 2,
189
+ channels: tuple[int, ...] = (32, 64, 128, 128),
190
+ kernel_size: int | Sequence[int] = 7,
191
+ pool_size: int | Sequence[int] = 2,
192
+ head_dim: int = 128,
193
+ dropout: float = 0.3,
194
+ learning_rate: float = 1e-3,
195
+ weight_decay: float = 0.0,
196
+ grad_clip_norm: float | None = None,
197
+ label_smoothing: float = 0.0,
198
+ loss: str = "cross_entropy",
199
+ focal_gamma: float = 2.0,
200
+ use_amp: bool = False,
201
+ swa_start_epoch: int | None = None,
202
+ tune_threshold: bool = False,
203
+ threshold_metric: str = "balanced_accuracy",
204
+ calibrate_temperature: bool = False,
205
+ min_val_auroc_for_threshold_tune: float = 0.6,
206
+ use_sam: bool = False,
207
+ sam_rho: float = 0.05,
208
+ batch_size: int = 32,
209
+ epochs: int = 100,
210
+ early_stopping_patience: int = 10,
211
+ val_fraction: float = 0.1,
212
+ warmup_epochs: int = 0,
213
+ standardize: bool = False,
214
+ input_transform: str | None = None,
215
+ warping: Any | None = None,
216
+ metrics_log_path: str | Path | None = None,
217
+ track_train_metrics: bool = False,
218
+ augment: Any | None = None,
219
+ mixup_alpha: float = 0.0,
220
+ cutmix_alpha: float = 0.0,
221
+ ema_decay: float | None = None,
222
+ retry_on_val_auroc_below: float | None = None,
223
+ max_retries: int = 2,
224
+ class_weight: str | np.ndarray | list | None = None,
225
+ device: str | torch.device = "auto",
226
+ random_state: int = 0,
227
+ verbose: bool = False,
228
+ ) -> None:
229
+ super().__init__(
230
+ input_dim=input_dim,
231
+ n_classes=n_classes,
232
+ learning_rate=learning_rate,
233
+ weight_decay=weight_decay,
234
+ grad_clip_norm=grad_clip_norm,
235
+ label_smoothing=label_smoothing,
236
+ loss=loss,
237
+ focal_gamma=focal_gamma,
238
+ use_amp=use_amp,
239
+ swa_start_epoch=swa_start_epoch,
240
+ tune_threshold=tune_threshold,
241
+ threshold_metric=threshold_metric,
242
+ calibrate_temperature=calibrate_temperature,
243
+ min_val_auroc_for_threshold_tune=min_val_auroc_for_threshold_tune,
244
+ use_sam=use_sam,
245
+ sam_rho=sam_rho,
246
+ batch_size=batch_size,
247
+ epochs=epochs,
248
+ early_stopping_patience=early_stopping_patience,
249
+ val_fraction=val_fraction,
250
+ warmup_epochs=warmup_epochs,
251
+ standardize=standardize,
252
+ input_transform=input_transform,
253
+ warping=warping,
254
+ metrics_log_path=metrics_log_path,
255
+ track_train_metrics=track_train_metrics,
256
+ augment=augment,
257
+ mixup_alpha=mixup_alpha,
258
+ cutmix_alpha=cutmix_alpha,
259
+ ema_decay=ema_decay,
260
+ retry_on_val_auroc_below=retry_on_val_auroc_below,
261
+ max_retries=max_retries,
262
+ class_weight=class_weight,
263
+ device=device,
264
+ random_state=random_state,
265
+ verbose=verbose,
266
+ )
267
+ self.channels = channels
268
+ self.kernel_size = kernel_size
269
+ self.pool_size = pool_size
270
+ self.head_dim = head_dim
271
+ self.dropout = dropout
272
+
273
+ def _build_model(self) -> nn.Module:
274
+ return SpectralCNN1D(
275
+ input_dim=self.input_dim_,
276
+ n_classes=self.n_classes_,
277
+ channels=tuple(self.channels),
278
+ kernel_size=self.kernel_size,
279
+ pool_size=self.pool_size,
280
+ head_dim=int(self.head_dim),
281
+ dropout=float(self.dropout),
282
+ )
283
+
284
+ @classmethod
285
+ def from_spectrum(
286
+ cls, bin_width: int, input_dim: int, **overrides
287
+ ) -> "MaldiCNNClassifier":
288
+ """Construct a classifier with ``kernel_size`` scaled for ``bin_width``.
289
+
290
+ Scales ``kernel_size`` inversely with ``bin_width`` relative to
291
+ the package reference (``bin_width=3``, ``kernel_size=7``).
292
+ Any keyword in ``**overrides`` wins over the auto-scaled value.
293
+
294
+ Parameters
295
+ ----------
296
+ bin_width : int
297
+ Bin width in Daltons (e.g. 3 for the MaldiAMRKit default,
298
+ 6 for coarser binning).
299
+ input_dim : int
300
+ Number of bins in the input. Stored on the classifier for
301
+ shape validation.
302
+ **overrides
303
+ Any additional keyword arguments override the scaled defaults.
304
+
305
+ Returns
306
+ -------
307
+ MaldiCNNClassifier
308
+ An unfitted estimator with ``kernel_size`` scaled for the
309
+ given ``bin_width``.
310
+ """
311
+ kwargs: dict[str, Any] = {
312
+ "input_dim": input_dim,
313
+ "kernel_size": scale_odd_kernel(bin_width),
314
+ }
315
+ kwargs.update(overrides)
316
+ return cls(**kwargs)
maldideepkit/py.typed ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ """1-D ResNet classifier for MALDI-TOF spectra."""
2
+
3
+ from .resnet import MaldiResNetClassifier, SpectralResNet1D
4
+
5
+ __all__ = ["MaldiResNetClassifier", "SpectralResNet1D"]