structboost 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.
- structboost/__init__.py +94 -0
- structboost/_annotation.py +266 -0
- structboost/_boosting.py +552 -0
- structboost/_decoder.py +109 -0
- structboost/_encoder.py +88 -0
- structboost/_explorer.py +842 -0
- structboost/_io.py +302 -0
- structboost/_model.py +3483 -0
- structboost/_persistence.py +326 -0
- structboost/_plotting.py +301 -0
- structboost/_simulation.py +867 -0
- structboost/_stability.py +412 -0
- structboost/_types.py +393 -0
- structboost/_utils.py +509 -0
- structboost/py.typed +0 -0
- structboost-0.1.0.dist-info/METADATA +219 -0
- structboost-0.1.0.dist-info/RECORD +19 -0
- structboost-0.1.0.dist-info/WHEEL +4 -0
- structboost-0.1.0.dist-info/licenses/LICENSE +21 -0
structboost/_types.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""Type definitions and configuration for BAE."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import numpy.typing as npt
|
|
10
|
+
|
|
11
|
+
# Type aliases
|
|
12
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
13
|
+
import torch
|
|
14
|
+
|
|
15
|
+
ArrayLike = npt.NDArray[np.floating] | torch.Tensor
|
|
16
|
+
Device = torch.device | str
|
|
17
|
+
else:
|
|
18
|
+
ArrayLike = npt.NDArray[np.floating] | Any
|
|
19
|
+
Device = str | Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class BAEConfig:
|
|
24
|
+
"""Configuration for Boosting Autoencoder.
|
|
25
|
+
|
|
26
|
+
The revised BAE architecture uses a linear encoder (optimized via boosting)
|
|
27
|
+
and an MLP decoder (optimized via SGD).
|
|
28
|
+
|
|
29
|
+
Attributes
|
|
30
|
+
----------
|
|
31
|
+
latent_dim
|
|
32
|
+
Number of latent dimensions.
|
|
33
|
+
decoder_hidden_dims
|
|
34
|
+
Hidden layer sizes for the decoder MLP, in forward order from the latent
|
|
35
|
+
code to the gene output. Default ``(64,)`` — a single hidden layer.
|
|
36
|
+
|
|
37
|
+
On simulated data with planted gene programs, one hidden layer of 64 units
|
|
38
|
+
recovered markers as well as or better than ``(128,)`` (marker-recovery F1
|
|
39
|
+
0.99–1.00 across four scenarios spanning 500–3000 genes), reconstructed to
|
|
40
|
+
~91–95% of the linear ceiling, and trained faster. A *linear* decoder
|
|
41
|
+
(``()``) is a valid faster, more interpretable choice but loses recall on
|
|
42
|
+
harder data (F1 down to 0.83, reconstruction 65–73% of ceiling). Deeper or
|
|
43
|
+
wider decoders did not help and often hurt marker recovery: a funnel such
|
|
44
|
+
as ``(128, 64)`` dropped to F1 0.80 because a narrow layer immediately
|
|
45
|
+
before the gene output distorts the reconstruction gradient that the
|
|
46
|
+
boosting target is built from — a BAE-specific effect, since that gradient
|
|
47
|
+
is what the encoder is fitted against. If more capacity is needed, widen
|
|
48
|
+
(``(64, 128)``) rather than narrowing toward the output. These are
|
|
49
|
+
simulation results; confirm on real data before assuming ``(64,)`` has
|
|
50
|
+
enough capacity for genuinely nonlinear expression structure.
|
|
51
|
+
decoder_activation
|
|
52
|
+
Activation function for decoder hidden layers. ``"tanh"`` (default) is the
|
|
53
|
+
most stable; ``"elu"`` matched it; ``"relu"`` was unstable at larger widths
|
|
54
|
+
(marker-recovery F1 collapsing to 0.80 at 256 units).
|
|
55
|
+
decoder_dropout_rate
|
|
56
|
+
Dropout rate for decoder (0.0 = no dropout). Benchmarks showed no benefit;
|
|
57
|
+
the default is 0.0.
|
|
58
|
+
decoder_use_batch_norm
|
|
59
|
+
Whether to use batch normalization in the decoder. Default False, and
|
|
60
|
+
changing it is not recommended: batch norm *lowers* reconstruction MSE but
|
|
61
|
+
badly degrades marker recovery (F1 0.59–0.73 in benchmarks). BAE computes
|
|
62
|
+
the boosting target with the decoder in eval mode (running statistics) and
|
|
63
|
+
then updates it in train mode (batch statistics), so batch norm makes the
|
|
64
|
+
target inconsistent with the decoder that produced it.
|
|
65
|
+
split_softmax
|
|
66
|
+
If True, apply split-softmax transformation between encoder and decoder.
|
|
67
|
+
Each latent dimension z_i is paired with −z_i (interleaved) and softmax-
|
|
68
|
+
normalized onto the 2d-dimensional simplex:
|
|
69
|
+
σ_split(z) = softmax((z_1, −z_1, ..., z_d, −z_d)).
|
|
70
|
+
The decoder then receives a 2d-dimensional compositional input,
|
|
71
|
+
enabling soft clustering of cells into 2d groups.
|
|
72
|
+
Default is False (standard unconstrained latent space).
|
|
73
|
+
boosting_stepno
|
|
74
|
+
Number of boosting iterations per training iteration.
|
|
75
|
+
boosting_nu
|
|
76
|
+
Boosting learning rate.
|
|
77
|
+
boosting_csf
|
|
78
|
+
Cumulative shrinkage factor for boosting.
|
|
79
|
+
boosting_independent
|
|
80
|
+
If True, reset boosting state for each latent dimension (recommended).
|
|
81
|
+
nuisance_ridge
|
|
82
|
+
Ridge strength for the batch covariates when batch_integration_mode
|
|
83
|
+
includes "encoder", expressed relative to each encoded column's
|
|
84
|
+
squared norm. Mandatory *gene* coefficients are never penalized. Default
|
|
85
|
+
0 preserves exact least squares.
|
|
86
|
+
|
|
87
|
+
This is a numerical-stability knob rather than a modelling one. Boosting
|
|
88
|
+
refits the mandatory block jointly at every step, and that block is the
|
|
89
|
+
mandatory genes together with the batch columns. When two of those are
|
|
90
|
+
collinear the solve has no unique answer and raises; when they are merely
|
|
91
|
+
close to collinear it returns a large, sign-unstable answer and raises
|
|
92
|
+
nothing. Reaching for a small value here, 1e-3 say, is the remedy in
|
|
93
|
+
the second case. It is never applied automatically, because ridge changes
|
|
94
|
+
the estimates and doing so silently would fit a different model than the
|
|
95
|
+
one asked for.
|
|
96
|
+
prior_mode
|
|
97
|
+
How the prior encoder weight matrix is treated when the model was built
|
|
98
|
+
by :meth:`BAE.from_reference`; ignored otherwise. ``"frozen"`` (default)
|
|
99
|
+
never boosts the prior columns, so they remain bitwise equal to the
|
|
100
|
+
reference matrix and the transferred gene programs are literally the
|
|
101
|
+
reference's. ``"anchored"`` boosts them too, but restarts each iteration
|
|
102
|
+
from the fixed original matrix rather than from the previous iteration's
|
|
103
|
+
result — boosting from an offset model, so deviation is bounded by
|
|
104
|
+
``boosting_stepno`` and never accumulates. Unanchored re-fitting is not
|
|
105
|
+
offered: a prior used merely as a starting point is forgotten within a
|
|
106
|
+
few hundred iterations, since the encoder support random-walks and two
|
|
107
|
+
runs end no more similar than two unrelated ones.
|
|
108
|
+
boosting_precompute_covcache
|
|
109
|
+
If True, compute the full p×p covariance matrix before training, which
|
|
110
|
+
costs 8·p² bytes (3.2 GB at p=20,000).
|
|
111
|
+
If False (default), `allboost` keeps a dict-backed column cache: a
|
|
112
|
+
covariance column is computed the first time its feature is selected and
|
|
113
|
+
reused across targets and training iterations. Memory then scales with
|
|
114
|
+
the number of *distinct selected* features (8·p bytes per column), not
|
|
115
|
+
with p², which is a large saving because boosting selects far fewer
|
|
116
|
+
features than are available. Precompute only when p is small enough that
|
|
117
|
+
the full matrix is comfortable and many features will be selected anyway.
|
|
118
|
+
disentanglement
|
|
119
|
+
Latent-dimension disentanglement method. ``"none"`` (default) applies no
|
|
120
|
+
constraint. ``"correlation"`` adds a soft, differentiable squared-
|
|
121
|
+
correlation penalty to the functional-gradient target objective and is the
|
|
122
|
+
recommended method. ``"leave_one_out"`` retains the earlier experimental
|
|
123
|
+
target residualization method, in which each target column is regressed on
|
|
124
|
+
all other original target columns. Leave-one-out reduces some redundancies
|
|
125
|
+
but does not mathematically produce mutually orthogonal residuals.
|
|
126
|
+
disentanglement_lambda
|
|
127
|
+
Strength of the soft correlation penalty. The penalty is normalized over
|
|
128
|
+
latent-dimension pairs and scaled with the number of cells so that this
|
|
129
|
+
value has the same meaning at different dataset sizes. Default ``1e-4`` is
|
|
130
|
+
a conservative starting point from simulations; it is not universally
|
|
131
|
+
optimal. A useful tuning grid is ``0, 1e-5, 3e-5, 1e-4, 3e-4, 1e-3``.
|
|
132
|
+
Only used when ``disentanglement="correlation"``.
|
|
133
|
+
disentanglement_standardize
|
|
134
|
+
If True with ``disentanglement="leave_one_out"``, standardize each target
|
|
135
|
+
column before residualization. This is unavailable for the correlation
|
|
136
|
+
method, whose loss already operates on standardized correlations.
|
|
137
|
+
standardize_targets
|
|
138
|
+
If True, standardize boosting targets (zero mean, unit variance per column)
|
|
139
|
+
before passing to allboost. Default is False.
|
|
140
|
+
|
|
141
|
+
This is a modelling choice, not a numerical convenience. For a fixed
|
|
142
|
+
decoder it does not change which genes `allboost` selects — rescaling a
|
|
143
|
+
target column by c scales the selection criterion by c², leaving every
|
|
144
|
+
argmax untouched and the coefficients scaled by exactly c — but it does
|
|
145
|
+
change the encoder magnitude, and therefore every later decoder update and
|
|
146
|
+
every later target. It equalises the target variance of strong and weak
|
|
147
|
+
latent dimensions, which constrains the latent scale and can stabilise
|
|
148
|
+
split-softmax, at the cost of promoting near-empty dimensions and
|
|
149
|
+
discarding the scale of an ``init_pca`` or ``init_obsm`` warm start. On
|
|
150
|
+
simulated data with planted gene programs it raises selection precision
|
|
151
|
+
but roughly halves recall relative to the default.
|
|
152
|
+
target_optim_lr
|
|
153
|
+
Step size for computing boosting targets via gradient descent on z.
|
|
154
|
+
Targets are computed as z* = z - lr * ∂L_target/∂z (single gradient step),
|
|
155
|
+
where L_target sums the squared error over cells and averages it over
|
|
156
|
+
genes. That convention — rather than the elementwise mean used for the
|
|
157
|
+
decoder update and every reported loss — makes a cell's target step
|
|
158
|
+
independent of how many cells the dataset contains, so a given
|
|
159
|
+
``target_optim_lr`` means the same thing at every dataset size.
|
|
160
|
+
|
|
161
|
+
.. note::
|
|
162
|
+
An earlier formulation took the target gradient from the elementwise
|
|
163
|
+
mean MSE, making it a factor ``n_cells`` smaller. A value ported from
|
|
164
|
+
such a setup must be divided by the number of cells.
|
|
165
|
+
decoder_lr
|
|
166
|
+
Learning rate for decoder SGD optimization.
|
|
167
|
+
decoder_weight_decay
|
|
168
|
+
Weight decay (L2 penalty) for the AdamW decoder optimizer.
|
|
169
|
+
Default is 0.0 (no penalty). Increase for decoder weight
|
|
170
|
+
regularisation (typical range: 1e-5 to 1e-2).
|
|
171
|
+
decoder_updates_per_iteration
|
|
172
|
+
Number of decoder SGD steps per training iteration.
|
|
173
|
+
max_iterations
|
|
174
|
+
Maximum number of training iterations.
|
|
175
|
+
enable_early_stopping
|
|
176
|
+
If True (default), stop training early when the checkpoint-selection loss
|
|
177
|
+
does not decrease for `early_stopping_patience` iterations. This is the
|
|
178
|
+
decoder training MSE for ``disentanglement="none"`` and
|
|
179
|
+
``"leave_one_out"``; for ``"correlation"`` it additionally includes the
|
|
180
|
+
weighted disentanglement penalty. If False, train for exactly
|
|
181
|
+
`max_iterations`.
|
|
182
|
+
early_stopping_patience
|
|
183
|
+
Number of consecutive iterations without checkpoint-selection loss
|
|
184
|
+
improvement required to trigger early stopping. Only used if
|
|
185
|
+
enable_early_stopping is True. Default is 50.
|
|
186
|
+
batch_size
|
|
187
|
+
Minibatch size for decoder SGD updates.
|
|
188
|
+
seed
|
|
189
|
+
Random seed for reproducibility. If None, no seed is set.
|
|
190
|
+
Controls train/val split, weight initialization, and SGD shuffling.
|
|
191
|
+
device
|
|
192
|
+
Device for computation ('cpu', 'cuda', or torch.device).
|
|
193
|
+
diagnostics
|
|
194
|
+
If True, collect per-iteration training diagnostics into a
|
|
195
|
+
:class:`TrainingReport` (see `BAE.training_report`) and additionally show
|
|
196
|
+
the relative encoder weight change (``dW``) and the number of selected
|
|
197
|
+
genes (``n_sel``) in the progress bar. This adds full-data forward and
|
|
198
|
+
backward passes per iteration, so it is off by default and
|
|
199
|
+
`diagnostics=False` costs exactly what training cost before. Diagnostics
|
|
200
|
+
are read-only: enabling them does not change the fitted model.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
latent_dim: int = 10
|
|
204
|
+
# Decoder MLP
|
|
205
|
+
decoder_hidden_dims: tuple[int, ...] = (64,)
|
|
206
|
+
decoder_activation: Literal["tanh", "relu", "leaky_relu", "elu"] = "tanh"
|
|
207
|
+
decoder_dropout_rate: float = 0.0
|
|
208
|
+
decoder_use_batch_norm: bool = False
|
|
209
|
+
split_softmax: bool = False
|
|
210
|
+
# Boosting parameters
|
|
211
|
+
boosting_stepno: int = 50
|
|
212
|
+
boosting_nu: float = 0.1
|
|
213
|
+
boosting_csf: float = 0.9
|
|
214
|
+
boosting_independent: bool = True
|
|
215
|
+
boosting_precompute_covcache: bool = False
|
|
216
|
+
nuisance_ridge: float = 0.0
|
|
217
|
+
prior_mode: Literal["frozen", "anchored"] = "frozen"
|
|
218
|
+
disentanglement: Literal["none", "correlation", "leave_one_out"] = "none"
|
|
219
|
+
disentanglement_lambda: float = 1e-4
|
|
220
|
+
disentanglement_standardize: bool = False
|
|
221
|
+
standardize_targets: bool = False
|
|
222
|
+
# Target computation
|
|
223
|
+
target_optim_lr: float = 1.0
|
|
224
|
+
# Training parameters
|
|
225
|
+
decoder_lr: float = 1e-3
|
|
226
|
+
decoder_weight_decay: float = 0.0
|
|
227
|
+
decoder_updates_per_iteration: int = 10
|
|
228
|
+
max_iterations: int = 1000
|
|
229
|
+
enable_early_stopping: bool = True
|
|
230
|
+
early_stopping_patience: int = 50
|
|
231
|
+
batch_size: int = 2**9
|
|
232
|
+
seed: int | None = None
|
|
233
|
+
# Device
|
|
234
|
+
device: Device = "cpu"
|
|
235
|
+
# Diagnostics
|
|
236
|
+
diagnostics: bool = False
|
|
237
|
+
|
|
238
|
+
def __post_init__(self) -> None:
|
|
239
|
+
"""Validate configuration."""
|
|
240
|
+
if self.latent_dim < 1:
|
|
241
|
+
raise ValueError("latent_dim must be >= 1")
|
|
242
|
+
if not 0.0 <= self.decoder_dropout_rate < 1.0:
|
|
243
|
+
raise ValueError("decoder_dropout_rate must be in [0.0, 1.0)")
|
|
244
|
+
if self.boosting_stepno < 1:
|
|
245
|
+
raise ValueError("boosting_stepno must be >= 1")
|
|
246
|
+
if not np.isfinite(self.nuisance_ridge) or self.nuisance_ridge < 0:
|
|
247
|
+
raise ValueError("nuisance_ridge must be finite and >= 0")
|
|
248
|
+
if self.prior_mode not in {"frozen", "anchored"}:
|
|
249
|
+
raise ValueError("prior_mode must be 'frozen' or 'anchored'")
|
|
250
|
+
if not 0.0 < self.boosting_nu <= 1.0:
|
|
251
|
+
raise ValueError("boosting_nu must be in (0.0, 1.0]")
|
|
252
|
+
if self.disentanglement not in {"none", "correlation", "leave_one_out"}:
|
|
253
|
+
raise ValueError(
|
|
254
|
+
"disentanglement must be one of 'none', 'correlation', or 'leave_one_out'"
|
|
255
|
+
)
|
|
256
|
+
if not np.isfinite(self.disentanglement_lambda) or self.disentanglement_lambda < 0:
|
|
257
|
+
raise ValueError("disentanglement_lambda must be finite and >= 0")
|
|
258
|
+
if self.disentanglement_standardize and self.disentanglement != "leave_one_out":
|
|
259
|
+
raise ValueError(
|
|
260
|
+
"disentanglement_standardize is only available with disentanglement='leave_one_out'"
|
|
261
|
+
)
|
|
262
|
+
if self.decoder_weight_decay < 0:
|
|
263
|
+
raise ValueError("decoder_weight_decay must be >= 0")
|
|
264
|
+
if self.max_iterations < 1:
|
|
265
|
+
raise ValueError("max_iterations must be >= 1")
|
|
266
|
+
if self.early_stopping_patience < 1:
|
|
267
|
+
raise ValueError("early_stopping_patience must be >= 1")
|
|
268
|
+
if self.batch_size < 1:
|
|
269
|
+
raise ValueError("batch_size must be >= 1")
|
|
270
|
+
if isinstance(self.device, str):
|
|
271
|
+
try:
|
|
272
|
+
import torch
|
|
273
|
+
|
|
274
|
+
self.device = torch.device(self.device)
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@dataclass(frozen=True)
|
|
280
|
+
class TrainingReport:
|
|
281
|
+
"""Per-iteration training diagnostics collected by ``BAE.fit``.
|
|
282
|
+
|
|
283
|
+
Produced only when ``diagnostics=True``. Every field is a NumPy array whose
|
|
284
|
+
first axis indexes the training iteration; per-dimension fields have shape
|
|
285
|
+
``(n_iterations, latent_dim)``.
|
|
286
|
+
|
|
287
|
+
Each BAE iteration alternates two optimizers, giving three points at which
|
|
288
|
+
the full-data reconstruction loss can be measured:
|
|
289
|
+
|
|
290
|
+
``A`` (``loss_pre_boost``)
|
|
291
|
+
before anything changes, i.e. encoder from the previous iteration.
|
|
292
|
+
``B`` (``loss_post_boost``)
|
|
293
|
+
after the encoder is re-fit by boosting, before the decoder moves.
|
|
294
|
+
``C`` (``loss_post_decoder``)
|
|
295
|
+
after the decoder SGD steps.
|
|
296
|
+
|
|
297
|
+
The differences ``encoder_delta = B - A`` and ``decoder_delta = C - B``
|
|
298
|
+
attribute the loss change of each iteration to the two halves of the
|
|
299
|
+
alternation, which is otherwise invisible: a single loss curve cannot say
|
|
300
|
+
whether the boosting step helps or whether the decoder is merely repairing
|
|
301
|
+
the damage it does.
|
|
302
|
+
|
|
303
|
+
Attributes
|
|
304
|
+
----------
|
|
305
|
+
iteration
|
|
306
|
+
0-based iteration index.
|
|
307
|
+
train_loss
|
|
308
|
+
Running average minibatch MSE during the decoder update. This is the
|
|
309
|
+
historical metric and a noisier proxy for ``loss_post_decoder``. Early
|
|
310
|
+
stopping uses it directly unless soft correlation disentanglement is
|
|
311
|
+
enabled, in which case the corresponding
|
|
312
|
+
``training_history["selection_loss"]`` also includes that penalty.
|
|
313
|
+
loss_pre_boost
|
|
314
|
+
Full-data reconstruction MSE at point A above.
|
|
315
|
+
loss_post_boost
|
|
316
|
+
Full-data reconstruction MSE at point B above.
|
|
317
|
+
loss_post_decoder
|
|
318
|
+
Full-data reconstruction MSE at point C above.
|
|
319
|
+
encoder_delta
|
|
320
|
+
Loss change attributable to the boosting step; negative means it reduced
|
|
321
|
+
the loss.
|
|
322
|
+
decoder_delta
|
|
323
|
+
Loss change attributable to the decoder step; negative means it reduced
|
|
324
|
+
the loss.
|
|
325
|
+
target_grad_norm
|
|
326
|
+
Norm of the boosting-target gradient, ``||dL/dz||``. This is the signal
|
|
327
|
+
the encoder is fitted against; if it collapses toward zero the boosting
|
|
328
|
+
targets carry no information.
|
|
329
|
+
decoder_grad_norm
|
|
330
|
+
Norm of the full-data decoder parameter gradient.
|
|
331
|
+
boosting_r2
|
|
332
|
+
R^2 of the encoder output against the boosting targets, i.e. how well
|
|
333
|
+
``allboost`` fitted what it was asked to fit.
|
|
334
|
+
encoder_weight_norm
|
|
335
|
+
Mean absolute encoder weight. Typically grows by orders of magnitude
|
|
336
|
+
during training, so weight *changes* must be read relative to it.
|
|
337
|
+
weight_change_rel
|
|
338
|
+
Mean absolute change of the encoder weights against the previous
|
|
339
|
+
iteration, divided by ``encoder_weight_norm``. The primary convergence
|
|
340
|
+
signal. ``NaN`` in the first iteration, which has no predecessor.
|
|
341
|
+
support_jaccard
|
|
342
|
+
Jaccard overlap of the selected-gene set against the previous iteration.
|
|
343
|
+
Answers whether the *gene set* has settled, which is coarser and noisier
|
|
344
|
+
than ``weight_change_rel`` but directly interpretable. ``NaN`` in the
|
|
345
|
+
first iteration.
|
|
346
|
+
min_dim_cosine
|
|
347
|
+
Smallest per-latent-dimension cosine similarity between consecutive
|
|
348
|
+
encoder weight matrices; scale-invariant, and identifies which dimension
|
|
349
|
+
is still moving. ``NaN`` in the first iteration.
|
|
350
|
+
n_selected
|
|
351
|
+
Number of genes with a nonzero weight in any latent dimension.
|
|
352
|
+
n_selected_per_dim
|
|
353
|
+
Number of genes selected per latent dimension.
|
|
354
|
+
latent_var_per_dim
|
|
355
|
+
Variance of each latent dimension across cells. Compare dimensions with
|
|
356
|
+
each other rather than against an absolute scale: the overall latent
|
|
357
|
+
magnitude is set by the boosting shrinkage and is small by construction.
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
iteration: npt.NDArray[np.intp]
|
|
361
|
+
train_loss: npt.NDArray[np.float64]
|
|
362
|
+
loss_pre_boost: npt.NDArray[np.float64]
|
|
363
|
+
loss_post_boost: npt.NDArray[np.float64]
|
|
364
|
+
loss_post_decoder: npt.NDArray[np.float64]
|
|
365
|
+
encoder_delta: npt.NDArray[np.float64]
|
|
366
|
+
decoder_delta: npt.NDArray[np.float64]
|
|
367
|
+
target_grad_norm: npt.NDArray[np.float64]
|
|
368
|
+
decoder_grad_norm: npt.NDArray[np.float64]
|
|
369
|
+
boosting_r2: npt.NDArray[np.float64]
|
|
370
|
+
encoder_weight_norm: npt.NDArray[np.float64]
|
|
371
|
+
weight_change_rel: npt.NDArray[np.float64]
|
|
372
|
+
support_jaccard: npt.NDArray[np.float64]
|
|
373
|
+
min_dim_cosine: npt.NDArray[np.float64]
|
|
374
|
+
n_selected: npt.NDArray[np.intp]
|
|
375
|
+
n_selected_per_dim: npt.NDArray[np.intp]
|
|
376
|
+
latent_var_per_dim: npt.NDArray[np.float64]
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def n_iterations(self) -> int:
|
|
380
|
+
"""Number of recorded training iterations."""
|
|
381
|
+
return int(self.iteration.shape[0])
|
|
382
|
+
|
|
383
|
+
def to_dict(self) -> dict[str, npt.NDArray[Any]]:
|
|
384
|
+
"""Return the report as a plain dict of arrays, ready for ``adata.uns``."""
|
|
385
|
+
return {f: getattr(self, f) for f in self.__dataclass_fields__}
|
|
386
|
+
|
|
387
|
+
@classmethod
|
|
388
|
+
def from_dict(cls, data: dict[str, npt.NDArray[Any]]) -> TrainingReport:
|
|
389
|
+
"""Rebuild a report from :meth:`to_dict` output, e.g. read back from h5ad."""
|
|
390
|
+
missing = [f for f in cls.__dataclass_fields__ if f not in data]
|
|
391
|
+
if missing:
|
|
392
|
+
raise KeyError(f"missing training report fields: {missing}")
|
|
393
|
+
return cls(**{f: np.asarray(data[f]) for f in cls.__dataclass_fields__})
|