deepecgkit 0.2.0__tar.gz → 0.3.0__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.
- deepecgkit-0.3.0/PKG-INFO +150 -0
- deepecgkit-0.3.0/README.md +113 -0
- deepecgkit-0.3.0/deepecgkit/__init__.py +104 -0
- deepecgkit-0.3.0/deepecgkit/_batching.py +115 -0
- deepecgkit-0.3.0/deepecgkit/_model_loading.py +90 -0
- deepecgkit-0.3.0/deepecgkit/analysis/__init__.py +71 -0
- deepecgkit-0.3.0/deepecgkit/analysis/aggregation.py +431 -0
- deepecgkit-0.3.0/deepecgkit/analysis/latex.py +730 -0
- deepecgkit-0.3.0/deepecgkit/analysis/ranking.py +666 -0
- deepecgkit-0.3.0/deepecgkit/analysis/statistics.py +379 -0
- deepecgkit-0.3.0/deepecgkit/cli/__init__.py +106 -0
- deepecgkit-0.3.0/deepecgkit/cli/_common.py +598 -0
- deepecgkit-0.3.0/deepecgkit/cli/commands/__init__.py +6 -0
- deepecgkit-0.3.0/deepecgkit/cli/commands/analysis.py +426 -0
- deepecgkit-0.3.0/deepecgkit/cli/commands/evaluate.py +476 -0
- deepecgkit-0.3.0/deepecgkit/cli/commands/explain.py +681 -0
- deepecgkit-0.3.0/deepecgkit/cli/commands/extract.py +413 -0
- deepecgkit-0.3.0/deepecgkit/cli/commands/info.py +210 -0
- deepecgkit-0.3.0/deepecgkit/cli/commands/predict.py +328 -0
- deepecgkit-0.3.0/deepecgkit/cli/commands/probe.py +600 -0
- deepecgkit-0.3.0/deepecgkit/cli/commands/train.py +609 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/cli/config.py +2 -2
- deepecgkit-0.3.0/deepecgkit/cli/defaults.py +45 -0
- deepecgkit-0.3.0/deepecgkit/cli/logger.py +67 -0
- deepecgkit-0.3.0/deepecgkit/cli/main.py +323 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/cli/registry.py +16 -6
- deepecgkit-0.3.0/deepecgkit/cli/run_dir.py +219 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/datasets/__init__.py +18 -0
- deepecgkit-0.3.0/deepecgkit/datasets/af_classification.py +358 -0
- deepecgkit-0.3.0/deepecgkit/datasets/base.py +474 -0
- deepecgkit-0.3.0/deepecgkit/datasets/io.py +273 -0
- deepecgkit-0.3.0/deepecgkit/datasets/ltafdb.py +407 -0
- deepecgkit-0.3.0/deepecgkit/datasets/mitbih_afdb.py +226 -0
- deepecgkit-0.3.0/deepecgkit/datasets/modules.py +327 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/datasets/preprocessing.py +146 -15
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/datasets/ptbxl.py +128 -104
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/datasets/splitting.py +31 -11
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/datasets/unified_af.py +159 -68
- deepecgkit-0.3.0/deepecgkit/evaluation/__init__.py +183 -0
- deepecgkit-0.3.0/deepecgkit/evaluation/calibration.py +420 -0
- deepecgkit-0.3.0/deepecgkit/evaluation/evaluator.py +731 -0
- deepecgkit-0.3.0/deepecgkit/evaluation/metrics.py +632 -0
- deepecgkit-0.3.0/deepecgkit/evaluation/report.py +390 -0
- deepecgkit-0.3.0/deepecgkit/evaluation/representation.py +973 -0
- deepecgkit-0.3.0/deepecgkit/evaluation/visualization.py +538 -0
- deepecgkit-0.3.0/deepecgkit/explainability/__init__.py +91 -0
- deepecgkit-0.3.0/deepecgkit/explainability/attribution.py +914 -0
- deepecgkit-0.3.0/deepecgkit/explainability/beats.py +293 -0
- deepecgkit-0.3.0/deepecgkit/explainability/constraints.py +154 -0
- deepecgkit-0.3.0/deepecgkit/explainability/visualization.py +209 -0
- deepecgkit-0.3.0/deepecgkit/features.py +439 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/__init__.py +28 -2
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/af_classifier.py +52 -60
- deepecgkit-0.3.0/deepecgkit/models/base.py +68 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/convnext_v2_1d.py +3 -11
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/crnn.py +11 -18
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/deep_res_cnn.py +34 -5
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/ecg_dualnet.py +11 -40
- deepecgkit-0.3.0/deepecgkit/models/factory.py +204 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/fcn_wang.py +9 -12
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/gru_model.py +6 -9
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/inception_time.py +2 -11
- deepecgkit-0.2.0/deepecgkit/models/kanres_wide_x.py → deepecgkit-0.3.0/deepecgkit/models/kanres_deep_x.py +10 -13
- deepecgkit-0.2.0/deepecgkit/models/kanres_x.py → deepecgkit-0.3.0/deepecgkit/models/kanres_wide_x.py +4 -52
- deepecgkit-0.3.0/deepecgkit/models/kanres_x.py +10 -0
- deepecgkit-0.3.0/deepecgkit/models/layers.py +74 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/lstm_model.py +10 -18
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/mamba1d.py +5 -12
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/medformer.py +14 -13
- deepecgkit-0.3.0/deepecgkit/models/mlp_wang.py +77 -0
- deepecgkit-0.3.0/deepecgkit/models/pretrained.py +238 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/resnet1d.py +10 -15
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/resnet1d_wang.py +2 -10
- deepecgkit-0.3.0/deepecgkit/models/resnet_hannun.py +154 -0
- deepecgkit-0.3.0/deepecgkit/models/rocket.py +95 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/se_resnet1d.py +7 -12
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/simple_cnn.py +4 -10
- deepecgkit-0.3.0/deepecgkit/models/spectrogram_cnn.py +103 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/tcn.py +2 -11
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/transformer_ecg.py +4 -37
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/xresnet1d.py +8 -12
- deepecgkit-0.3.0/deepecgkit/models/xresnet1d101_benchmark.py +62 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/models/xresnet1d_benchmark.py +19 -24
- deepecgkit-0.3.0/deepecgkit/py.typed +0 -0
- deepecgkit-0.3.0/deepecgkit/registry.py +185 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/training/train.py +223 -84
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/utils/__init__.py +6 -5
- deepecgkit-0.3.0/deepecgkit/utils/checkpoints.py +46 -0
- deepecgkit-0.3.0/deepecgkit/utils/device.py +74 -0
- deepecgkit-0.3.0/deepecgkit/utils/download.py +512 -0
- deepecgkit-0.3.0/deepecgkit/utils/paths.py +26 -0
- deepecgkit-0.3.0/deepecgkit/utils/plotting.py +28 -0
- deepecgkit-0.3.0/deepecgkit/utils/provenance.py +129 -0
- deepecgkit-0.3.0/deepecgkit/utils/weights.py +148 -0
- deepecgkit-0.3.0/deepecgkit.egg-info/PKG-INFO +150 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit.egg-info/SOURCES.txt +54 -1
- deepecgkit-0.3.0/deepecgkit.egg-info/requires.txt +22 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/pyproject.toml +51 -44
- deepecgkit-0.3.0/tests/test_analysis_aggregation.py +269 -0
- deepecgkit-0.3.0/tests/test_analysis_latex.py +337 -0
- deepecgkit-0.3.0/tests/test_analysis_ranking.py +348 -0
- deepecgkit-0.3.0/tests/test_analysis_statistics.py +218 -0
- deepecgkit-0.3.0/tests/test_bugfixes.py +271 -0
- deepecgkit-0.3.0/tests/test_cli.py +2329 -0
- deepecgkit-0.3.0/tests/test_datasets.py +1659 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/tests/test_deepecgkit.py +125 -21
- deepecgkit-0.3.0/tests/test_device.py +118 -0
- deepecgkit-0.3.0/tests/test_docs.py +263 -0
- deepecgkit-0.3.0/tests/test_evaluation.py +1387 -0
- deepecgkit-0.3.0/tests/test_examples.py +251 -0
- deepecgkit-0.3.0/tests/test_explainability.py +967 -0
- deepecgkit-0.3.0/tests/test_features.py +576 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/tests/test_models.py +166 -8
- deepecgkit-0.3.0/tests/test_protocol_fixes.py +270 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/tests/test_registry.py +13 -3
- deepecgkit-0.3.0/tests/test_representation.py +569 -0
- deepecgkit-0.3.0/tests/test_run_experiments.py +381 -0
- deepecgkit-0.3.0/tests/test_state_dict_compat.py +65 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/tests/test_training.py +263 -16
- deepecgkit-0.3.0/tests/test_utils.py +712 -0
- deepecgkit-0.2.0/PKG-INFO +0 -103
- deepecgkit-0.2.0/README.md +0 -67
- deepecgkit-0.2.0/deepecgkit/__init__.py +0 -17
- deepecgkit-0.2.0/deepecgkit/cli/__init__.py +0 -61
- deepecgkit-0.2.0/deepecgkit/cli/commands/__init__.py +0 -16
- deepecgkit-0.2.0/deepecgkit/cli/commands/evaluate.py +0 -89
- deepecgkit-0.2.0/deepecgkit/cli/commands/info.py +0 -73
- deepecgkit-0.2.0/deepecgkit/cli/commands/predict.py +0 -108
- deepecgkit-0.2.0/deepecgkit/cli/commands/train.py +0 -279
- deepecgkit-0.2.0/deepecgkit/cli/evaluation.py +0 -251
- deepecgkit-0.2.0/deepecgkit/cli/logger.py +0 -26
- deepecgkit-0.2.0/deepecgkit/cli/main.py +0 -456
- deepecgkit-0.2.0/deepecgkit/datasets/af_classification.py +0 -301
- deepecgkit-0.2.0/deepecgkit/datasets/base.py +0 -146
- deepecgkit-0.2.0/deepecgkit/datasets/ltafdb.py +0 -363
- deepecgkit-0.2.0/deepecgkit/datasets/mitbih_afdb.py +0 -287
- deepecgkit-0.2.0/deepecgkit/datasets/modules.py +0 -223
- deepecgkit-0.2.0/deepecgkit/evaluation/__init__.py +0 -40
- deepecgkit-0.2.0/deepecgkit/evaluation/evaluator.py +0 -392
- deepecgkit-0.2.0/deepecgkit/evaluation/metrics.py +0 -164
- deepecgkit-0.2.0/deepecgkit/evaluation/visualization.py +0 -280
- deepecgkit-0.2.0/deepecgkit/registry.py +0 -97
- deepecgkit-0.2.0/deepecgkit/utils/download.py +0 -123
- deepecgkit-0.2.0/deepecgkit/utils/weights.py +0 -107
- deepecgkit-0.2.0/deepecgkit.egg-info/PKG-INFO +0 -103
- deepecgkit-0.2.0/deepecgkit.egg-info/requires.txt +0 -25
- deepecgkit-0.2.0/tests/test_cli.py +0 -757
- deepecgkit-0.2.0/tests/test_datasets.py +0 -636
- deepecgkit-0.2.0/tests/test_evaluation.py +0 -370
- deepecgkit-0.2.0/tests/test_utils.py +0 -212
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/LICENSE +0 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/__main__.py +0 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit/training/__init__.py +0 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit.egg-info/dependency_links.txt +0 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit.egg-info/entry_points.txt +0 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/deepecgkit.egg-info/top_level.txt +0 -0
- {deepecgkit-0.2.0 → deepecgkit-0.3.0}/setup.cfg +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepecgkit
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A deep learning toolkit for ECG analysis
|
|
5
|
+
Author-email: Steven <steven@simula.no>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: numpy>=1.22.0
|
|
17
|
+
Requires-Dist: scipy>=1.8.0
|
|
18
|
+
Requires-Dist: torch>=2.1.0
|
|
19
|
+
Requires-Dist: pandas>=1.4.0
|
|
20
|
+
Requires-Dist: scikit-learn>=1.1.0
|
|
21
|
+
Requires-Dist: matplotlib>=3.5.0
|
|
22
|
+
Requires-Dist: wfdb>=4.0.0
|
|
23
|
+
Requires-Dist: seaborn>=0.11.2
|
|
24
|
+
Requires-Dist: tqdm>=4.60.0
|
|
25
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.3.5; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=3.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.11.13; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
31
|
+
Provides-Extra: docs
|
|
32
|
+
Requires-Dist: mkdocs>=1.6.0; extra == "docs"
|
|
33
|
+
Requires-Dist: mkdocs-material>=9.5.0; extra == "docs"
|
|
34
|
+
Requires-Dist: mkdocstrings[python]>=0.25.0; extra == "docs"
|
|
35
|
+
Requires-Dist: mkdocs-autorefs>=1.0.0; extra == "docs"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# DeepECG-Kit
|
|
39
|
+
|
|
40
|
+
A deep learning library for ECG analysis and arrhythmia classification. Provides a CLI for
|
|
41
|
+
training, evaluation, inference, feature extraction, and explainability across 26 model
|
|
42
|
+
architectures and 5 ECG datasets.
|
|
43
|
+
|
|
44
|
+
**Documentation:** https://stevenah.github.io/deepecg-kit/
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install deepecgkit
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For local development (Python ≥ 3.10):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv sync --group dev # blessed path
|
|
56
|
+
pip install -e ".[dev]" # pip alternative
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow.
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
deepecg train -m kanres -d af-classification # train (downloads the dataset, ~100 MB)
|
|
65
|
+
deepecg evaluate --checkpoint runs/<run> -d af-classification
|
|
66
|
+
deepecg predict --checkpoint runs/<run> --input ecg.npy --sampling-rate 300
|
|
67
|
+
deepecg extract-features --checkpoint runs/<run> -d af-classification -o features.npz
|
|
68
|
+
deepecg explain --checkpoint runs/<run> --input ecg.npy -o explanations/
|
|
69
|
+
deepecg list-models # all 26 architectures
|
|
70
|
+
deepecg list-datasets # sizes, signal shape, local status
|
|
71
|
+
deepecg info -m kanres
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Training writes a run directory `runs/{timestamp}-{model}-{dataset}-seed{seed}/` containing
|
|
75
|
+
checkpoints (top 3 by validation loss), training plots, test metrics
|
|
76
|
+
(`classification_metrics.csv`, confusion matrix, calibration curve), and run metadata, and ends
|
|
77
|
+
by printing the best checkpoint. Anywhere a checkpoint is expected, the run directory can be
|
|
78
|
+
passed instead and stands for that best checkpoint.
|
|
79
|
+
|
|
80
|
+
Checkpoints describe themselves: they record the architecture, the class names, and the
|
|
81
|
+
preprocessing the training data went through (sampling rate, segment length, normalisation).
|
|
82
|
+
That is why `--model` is optional above, why `deepecg evaluate` rebuilds the dataset the way
|
|
83
|
+
the model was trained, and why `deepecg predict` can take a raw recording: it is resampled,
|
|
84
|
+
segmented and normalised to match before it reaches the model.
|
|
85
|
+
|
|
86
|
+
Run `deepecg list-models` and `deepecg list-datasets` for the full catalogs — datasets cover
|
|
87
|
+
PhysioNet 2017 (`af-classification`), PTB-XL (`ptbxl`), MIT-BIH AFDB (`mitbih-afdb`),
|
|
88
|
+
Long-Term AFDB (`ltafdb`), and the combined `unified-af`. Each dataset is used at its own
|
|
89
|
+
sampling rate (300 Hz for the AF datasets, 500 Hz for PTB-XL) unless `--sampling-rate` says
|
|
90
|
+
otherwise.
|
|
91
|
+
|
|
92
|
+
Datasets are downloaded on first use into `~/.deepecgkit/datasets`; set `DEEPECGKIT_HOME` to
|
|
93
|
+
keep them somewhere else. Downloads come from PhysioNet's open-data mirror, fall back to
|
|
94
|
+
physionet.org, and resume if interrupted.
|
|
95
|
+
|
|
96
|
+
## Experiments
|
|
97
|
+
|
|
98
|
+
Experiment sweeps are defined in `experiments.yaml` and run via
|
|
99
|
+
`python scripts/run_experiments.py`. Each experiment trains every configured model on every
|
|
100
|
+
dataset across multiple seeds (default `seeds: [42, 43, 44, 45, 46]` — 5 runs per
|
|
101
|
+
model/dataset pair), saving to `runs/{model}-{dataset}-seed{seed}/`.
|
|
102
|
+
|
|
103
|
+
Aggregate and analyse finished runs with:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
deepecg aggregate --runs-dir runs --output-dir analysis --emit-latex
|
|
107
|
+
deepecg rank --runs-dir runs --metric f1
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Default Training Parameters
|
|
111
|
+
|
|
112
|
+
Defined once in `deepecgkit/cli/defaults.py`:
|
|
113
|
+
|
|
114
|
+
| Parameter | Value |
|
|
115
|
+
|-----------|-------|
|
|
116
|
+
| Epochs | 50 |
|
|
117
|
+
| Batch size | 32 |
|
|
118
|
+
| Learning rate | 0.001 |
|
|
119
|
+
| Optimizer | Adam |
|
|
120
|
+
| LR scheduler | ReduceLROnPlateau (factor=0.5, patience=5) |
|
|
121
|
+
| Early stopping patience | 10 |
|
|
122
|
+
| Validation split | 0.2 |
|
|
123
|
+
| Test split | 0.1 |
|
|
124
|
+
| Seed | 42 |
|
|
125
|
+
| Sampling rate | the dataset's own (override with `--sampling-rate`) |
|
|
126
|
+
| Data loader workers | 0 |
|
|
127
|
+
|
|
128
|
+
## References
|
|
129
|
+
|
|
130
|
+
### Datasets
|
|
131
|
+
|
|
132
|
+
1. **af-classification** — Clifford GD, Liu C, Moody B, Li-wei HL, Silva I, Li Q, Johnson AE, Mark RG. "AF Classification from a Short Single Lead ECG Recording: The PhysioNet/Computing in Cardiology Challenge 2017." *Computing in Cardiology (CinC)*, 2017. [[PhysioNet]](https://physionet.org/content/challenge-2017/1.0.0/)
|
|
133
|
+
|
|
134
|
+
2. **ptbxl** — Wagner P, Strodthoff N, Bousseljot RD, Kreiseler D, Lunze FI, Samek W, Schaeffter T. "PTB-XL, a Large Publicly Available Electrocardiography Dataset." *Scientific Data*, 7(1):154, 2020. [[PhysioNet]](https://physionet.org/content/ptb-xl/1.0.3/)
|
|
135
|
+
|
|
136
|
+
3. **mitbih-afdb** — Moody GB, Mark RG. "A New Method for Detecting Atrial Fibrillation Using R-R Intervals." *Computers in Cardiology*, 10:227–230, 1983. [[PhysioNet]](https://physionet.org/content/afdb/1.0.0/)
|
|
137
|
+
|
|
138
|
+
4. **ltafdb** — Petrutiu S, Sahakian AV, Swiryn S. "Abrupt Changes in Fibrillatory Wave Characteristics at the Termination of Paroxysmal Atrial Fibrillation in Humans." *Europace*, 9(7):466–470, 2007. [[PhysioNet]](https://physionet.org/content/ltafdb/1.0.0/)
|
|
139
|
+
|
|
140
|
+
5. **unified-af** — Combined cross-dataset using af-classification, mitbih-afdb, and ltafdb (see references above).
|
|
141
|
+
|
|
142
|
+
### Models
|
|
143
|
+
|
|
144
|
+
6. **convnext-v2** — Woo S, Debnath S, Hu R, Chen X, Liu Z, Kweon IS, Xie S. "ConvNeXt V2: Co-designing and Scaling ConvNets with Masked Autoencoders." *CVPR*, 2023. [[GitHub]](https://github.com/facebookresearch/ConvNeXt-V2)
|
|
145
|
+
|
|
146
|
+
7. **deep-res-cnn** — Elyamani H. "ECG Classification Using Deep Residual CNN." 2022. [[GitHub]](https://github.com/HaneenElyamani/ECG-classification)
|
|
147
|
+
|
|
148
|
+
8. **inception-time** — Fawaz HI, Lucas B, Forestier G, Pelletier C, Schmidt DF, Weber J, Webb GI, Idoumghar L, Muller PA, Petitjean F. "InceptionTime: Finding AlexNet for Time Series Classification." *Data Mining and Knowledge Discovery*, 34:1936–1962, 2020. [[GitHub]](https://github.com/hfawaz/InceptionTime)
|
|
149
|
+
|
|
150
|
+
9. **xresnet / xresnet1d-benchmark** — Strodthoff N, Wagner P, Schaeffter T, Samek W. "Deep Learning for ECG Analysis: Benchmarks and Insights from PTB-XL." *IEEE Journal of Biomedical and Health Informatics*, 25(5):1519–1528, 2021. [[GitHub]](https://github.com/helme/ecg_ptbxl_benchmarking)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# DeepECG-Kit
|
|
2
|
+
|
|
3
|
+
A deep learning library for ECG analysis and arrhythmia classification. Provides a CLI for
|
|
4
|
+
training, evaluation, inference, feature extraction, and explainability across 26 model
|
|
5
|
+
architectures and 5 ECG datasets.
|
|
6
|
+
|
|
7
|
+
**Documentation:** https://stevenah.github.io/deepecg-kit/
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install deepecgkit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For local development (Python ≥ 3.10):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv sync --group dev # blessed path
|
|
19
|
+
pip install -e ".[dev]" # pip alternative
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
deepecg train -m kanres -d af-classification # train (downloads the dataset, ~100 MB)
|
|
28
|
+
deepecg evaluate --checkpoint runs/<run> -d af-classification
|
|
29
|
+
deepecg predict --checkpoint runs/<run> --input ecg.npy --sampling-rate 300
|
|
30
|
+
deepecg extract-features --checkpoint runs/<run> -d af-classification -o features.npz
|
|
31
|
+
deepecg explain --checkpoint runs/<run> --input ecg.npy -o explanations/
|
|
32
|
+
deepecg list-models # all 26 architectures
|
|
33
|
+
deepecg list-datasets # sizes, signal shape, local status
|
|
34
|
+
deepecg info -m kanres
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Training writes a run directory `runs/{timestamp}-{model}-{dataset}-seed{seed}/` containing
|
|
38
|
+
checkpoints (top 3 by validation loss), training plots, test metrics
|
|
39
|
+
(`classification_metrics.csv`, confusion matrix, calibration curve), and run metadata, and ends
|
|
40
|
+
by printing the best checkpoint. Anywhere a checkpoint is expected, the run directory can be
|
|
41
|
+
passed instead and stands for that best checkpoint.
|
|
42
|
+
|
|
43
|
+
Checkpoints describe themselves: they record the architecture, the class names, and the
|
|
44
|
+
preprocessing the training data went through (sampling rate, segment length, normalisation).
|
|
45
|
+
That is why `--model` is optional above, why `deepecg evaluate` rebuilds the dataset the way
|
|
46
|
+
the model was trained, and why `deepecg predict` can take a raw recording: it is resampled,
|
|
47
|
+
segmented and normalised to match before it reaches the model.
|
|
48
|
+
|
|
49
|
+
Run `deepecg list-models` and `deepecg list-datasets` for the full catalogs — datasets cover
|
|
50
|
+
PhysioNet 2017 (`af-classification`), PTB-XL (`ptbxl`), MIT-BIH AFDB (`mitbih-afdb`),
|
|
51
|
+
Long-Term AFDB (`ltafdb`), and the combined `unified-af`. Each dataset is used at its own
|
|
52
|
+
sampling rate (300 Hz for the AF datasets, 500 Hz for PTB-XL) unless `--sampling-rate` says
|
|
53
|
+
otherwise.
|
|
54
|
+
|
|
55
|
+
Datasets are downloaded on first use into `~/.deepecgkit/datasets`; set `DEEPECGKIT_HOME` to
|
|
56
|
+
keep them somewhere else. Downloads come from PhysioNet's open-data mirror, fall back to
|
|
57
|
+
physionet.org, and resume if interrupted.
|
|
58
|
+
|
|
59
|
+
## Experiments
|
|
60
|
+
|
|
61
|
+
Experiment sweeps are defined in `experiments.yaml` and run via
|
|
62
|
+
`python scripts/run_experiments.py`. Each experiment trains every configured model on every
|
|
63
|
+
dataset across multiple seeds (default `seeds: [42, 43, 44, 45, 46]` — 5 runs per
|
|
64
|
+
model/dataset pair), saving to `runs/{model}-{dataset}-seed{seed}/`.
|
|
65
|
+
|
|
66
|
+
Aggregate and analyse finished runs with:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
deepecg aggregate --runs-dir runs --output-dir analysis --emit-latex
|
|
70
|
+
deepecg rank --runs-dir runs --metric f1
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Default Training Parameters
|
|
74
|
+
|
|
75
|
+
Defined once in `deepecgkit/cli/defaults.py`:
|
|
76
|
+
|
|
77
|
+
| Parameter | Value |
|
|
78
|
+
|-----------|-------|
|
|
79
|
+
| Epochs | 50 |
|
|
80
|
+
| Batch size | 32 |
|
|
81
|
+
| Learning rate | 0.001 |
|
|
82
|
+
| Optimizer | Adam |
|
|
83
|
+
| LR scheduler | ReduceLROnPlateau (factor=0.5, patience=5) |
|
|
84
|
+
| Early stopping patience | 10 |
|
|
85
|
+
| Validation split | 0.2 |
|
|
86
|
+
| Test split | 0.1 |
|
|
87
|
+
| Seed | 42 |
|
|
88
|
+
| Sampling rate | the dataset's own (override with `--sampling-rate`) |
|
|
89
|
+
| Data loader workers | 0 |
|
|
90
|
+
|
|
91
|
+
## References
|
|
92
|
+
|
|
93
|
+
### Datasets
|
|
94
|
+
|
|
95
|
+
1. **af-classification** — Clifford GD, Liu C, Moody B, Li-wei HL, Silva I, Li Q, Johnson AE, Mark RG. "AF Classification from a Short Single Lead ECG Recording: The PhysioNet/Computing in Cardiology Challenge 2017." *Computing in Cardiology (CinC)*, 2017. [[PhysioNet]](https://physionet.org/content/challenge-2017/1.0.0/)
|
|
96
|
+
|
|
97
|
+
2. **ptbxl** — Wagner P, Strodthoff N, Bousseljot RD, Kreiseler D, Lunze FI, Samek W, Schaeffter T. "PTB-XL, a Large Publicly Available Electrocardiography Dataset." *Scientific Data*, 7(1):154, 2020. [[PhysioNet]](https://physionet.org/content/ptb-xl/1.0.3/)
|
|
98
|
+
|
|
99
|
+
3. **mitbih-afdb** — Moody GB, Mark RG. "A New Method for Detecting Atrial Fibrillation Using R-R Intervals." *Computers in Cardiology*, 10:227–230, 1983. [[PhysioNet]](https://physionet.org/content/afdb/1.0.0/)
|
|
100
|
+
|
|
101
|
+
4. **ltafdb** — Petrutiu S, Sahakian AV, Swiryn S. "Abrupt Changes in Fibrillatory Wave Characteristics at the Termination of Paroxysmal Atrial Fibrillation in Humans." *Europace*, 9(7):466–470, 2007. [[PhysioNet]](https://physionet.org/content/ltafdb/1.0.0/)
|
|
102
|
+
|
|
103
|
+
5. **unified-af** — Combined cross-dataset using af-classification, mitbih-afdb, and ltafdb (see references above).
|
|
104
|
+
|
|
105
|
+
### Models
|
|
106
|
+
|
|
107
|
+
6. **convnext-v2** — Woo S, Debnath S, Hu R, Chen X, Liu Z, Kweon IS, Xie S. "ConvNeXt V2: Co-designing and Scaling ConvNets with Masked Autoencoders." *CVPR*, 2023. [[GitHub]](https://github.com/facebookresearch/ConvNeXt-V2)
|
|
108
|
+
|
|
109
|
+
7. **deep-res-cnn** — Elyamani H. "ECG Classification Using Deep Residual CNN." 2022. [[GitHub]](https://github.com/HaneenElyamani/ECG-classification)
|
|
110
|
+
|
|
111
|
+
8. **inception-time** — Fawaz HI, Lucas B, Forestier G, Pelletier C, Schmidt DF, Weber J, Webb GI, Idoumghar L, Muller PA, Petitjean F. "InceptionTime: Finding AlexNet for Time Series Classification." *Data Mining and Knowledge Discovery*, 34:1936–1962, 2020. [[GitHub]](https://github.com/hfawaz/InceptionTime)
|
|
112
|
+
|
|
113
|
+
9. **xresnet / xresnet1d-benchmark** — Strodthoff N, Wagner P, Schaeffter T, Samek W. "Deep Learning for ECG Analysis: Benchmarks and Insights from PTB-XL." *IEEE Journal of Biomedical and Health Informatics*, 25(5):1519–1528, 2021. [[GitHub]](https://github.com/helme/ecg_ptbxl_benchmarking)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""DeepECG-Kit: Deep learning library for ECG analysis and arrhythmia classification."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from deepecgkit.datasets import ECGDataModule
|
|
9
|
+
from deepecgkit.evaluation import ECGEvaluator
|
|
10
|
+
from deepecgkit.explainability import Attribution, ECGExplainer, explain
|
|
11
|
+
from deepecgkit.features import FeatureExtractor, FeatureSet, extract_features
|
|
12
|
+
from deepecgkit.models import KanResWideX
|
|
13
|
+
from deepecgkit.models.factory import build_model, build_model_from_checkpoint
|
|
14
|
+
from deepecgkit.registry import (
|
|
15
|
+
get_dataset_names,
|
|
16
|
+
get_model_names,
|
|
17
|
+
register_dataset,
|
|
18
|
+
register_model,
|
|
19
|
+
)
|
|
20
|
+
from deepecgkit.training import ECGTrainer
|
|
21
|
+
from deepecgkit.utils import read_csv
|
|
22
|
+
from deepecgkit.utils.device import resolve_device
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
__version__ = version("deepecgkit")
|
|
26
|
+
except PackageNotFoundError:
|
|
27
|
+
__version__ = "0.0.0.dev0"
|
|
28
|
+
|
|
29
|
+
__author__ = "DeepECG Kit Contributors"
|
|
30
|
+
__license__ = "Apache-2.0"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"Attribution",
|
|
34
|
+
"ECGDataModule",
|
|
35
|
+
"ECGEvaluator",
|
|
36
|
+
"ECGExplainer",
|
|
37
|
+
"ECGTrainer",
|
|
38
|
+
"FeatureExtractor",
|
|
39
|
+
"FeatureSet",
|
|
40
|
+
"KanResWideX",
|
|
41
|
+
"build_model",
|
|
42
|
+
"build_model_from_checkpoint",
|
|
43
|
+
"explain",
|
|
44
|
+
"extract_features",
|
|
45
|
+
"get_dataset_names",
|
|
46
|
+
"get_model_names",
|
|
47
|
+
"read_csv",
|
|
48
|
+
"register_dataset",
|
|
49
|
+
"register_model",
|
|
50
|
+
"resolve_device",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
#: Where each public name lives. A name is imported when it is first asked
|
|
54
|
+
#: for, so `import deepecgkit` and the commands that need little of the
|
|
55
|
+
#: package do not load PyTorch and the rest of the scientific stack.
|
|
56
|
+
_EXPORTS = {
|
|
57
|
+
"Attribution": "deepecgkit.explainability",
|
|
58
|
+
"ECGDataModule": "deepecgkit.datasets",
|
|
59
|
+
"ECGEvaluator": "deepecgkit.evaluation",
|
|
60
|
+
"ECGExplainer": "deepecgkit.explainability",
|
|
61
|
+
"ECGTrainer": "deepecgkit.training",
|
|
62
|
+
"FeatureExtractor": "deepecgkit.features",
|
|
63
|
+
"FeatureSet": "deepecgkit.features",
|
|
64
|
+
"KanResWideX": "deepecgkit.models",
|
|
65
|
+
"build_model": "deepecgkit.models.factory",
|
|
66
|
+
"build_model_from_checkpoint": "deepecgkit.models.factory",
|
|
67
|
+
"explain": "deepecgkit.explainability",
|
|
68
|
+
"extract_features": "deepecgkit.features",
|
|
69
|
+
"get_dataset_names": "deepecgkit.registry",
|
|
70
|
+
"get_model_names": "deepecgkit.registry",
|
|
71
|
+
"read_csv": "deepecgkit.utils",
|
|
72
|
+
"register_dataset": "deepecgkit.registry",
|
|
73
|
+
"register_model": "deepecgkit.registry",
|
|
74
|
+
"resolve_device": "deepecgkit.utils.device",
|
|
75
|
+
}
|
|
76
|
+
_SUBMODULES = frozenset(
|
|
77
|
+
{
|
|
78
|
+
"analysis",
|
|
79
|
+
"cli",
|
|
80
|
+
"datasets",
|
|
81
|
+
"evaluation",
|
|
82
|
+
"explainability",
|
|
83
|
+
"features",
|
|
84
|
+
"models",
|
|
85
|
+
"registry",
|
|
86
|
+
"training",
|
|
87
|
+
"utils",
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def __getattr__(name: str) -> Any:
|
|
93
|
+
if name in _EXPORTS:
|
|
94
|
+
value = getattr(importlib.import_module(_EXPORTS[name]), name)
|
|
95
|
+
elif name in _SUBMODULES:
|
|
96
|
+
value = importlib.import_module(f"{__name__}.{name}")
|
|
97
|
+
else:
|
|
98
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
99
|
+
globals()[name] = value
|
|
100
|
+
return value
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def __dir__() -> list[str]:
|
|
104
|
+
return sorted({*globals(), *__all__, *_SUBMODULES})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Shared input normalisation for model wrappers (internal).
|
|
2
|
+
|
|
3
|
+
FeatureExtractor and ECGExplainer accept the same family of inputs — arrays,
|
|
4
|
+
tensors, recording files, Datasets, DataLoaders, and ECGDataModules — and both
|
|
5
|
+
need them as a non-shuffling DataLoader so row order stays joinable to external
|
|
6
|
+
metadata. The dispatch lives here once.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import warnings
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import torch
|
|
15
|
+
from torch.utils.data import DataLoader, Dataset, RandomSampler, TensorDataset
|
|
16
|
+
|
|
17
|
+
from deepecgkit.datasets.io import as_model_input, read_ecg_recording
|
|
18
|
+
from deepecgkit.datasets.modules import ECGDataModule
|
|
19
|
+
|
|
20
|
+
__all__ = ["as_inference_dataloader", "unpack_batch"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def as_inference_dataloader(
|
|
24
|
+
data: Any,
|
|
25
|
+
*,
|
|
26
|
+
split: str,
|
|
27
|
+
batch_size: int,
|
|
28
|
+
num_workers: int,
|
|
29
|
+
input_channels: int | None,
|
|
30
|
+
verb: str = "processed",
|
|
31
|
+
) -> DataLoader:
|
|
32
|
+
"""Normalise any supported input to a non-shuffling DataLoader.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
data: An array, tensor, file path, Dataset, DataLoader, or ECGDataModule
|
|
36
|
+
split: Split name, used only for an ECGDataModule; "all" selects the
|
|
37
|
+
full dataset
|
|
38
|
+
batch_size: Batch size for loaders constructed here
|
|
39
|
+
num_workers: Worker count for loaders constructed here
|
|
40
|
+
input_channels: Lead count used to shape raw arrays for the model
|
|
41
|
+
verb: Past-tense verb naming the operation ("extracted", "explained"),
|
|
42
|
+
used in the shuffling warning
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
A loader yielding either signals alone or (signals, labels).
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
TypeError: If the input type is not supported.
|
|
49
|
+
RuntimeError: If a data module split is unavailable.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def build_loader(dataset: Dataset) -> DataLoader:
|
|
53
|
+
return DataLoader(
|
|
54
|
+
dataset,
|
|
55
|
+
batch_size=batch_size,
|
|
56
|
+
shuffle=False,
|
|
57
|
+
num_workers=num_workers,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if isinstance(data, ECGDataModule):
|
|
61
|
+
# The module's own train_dataloader shuffles, which would scramble row
|
|
62
|
+
# order, so the loader is constructed here instead.
|
|
63
|
+
if data.dataset is None or data.train_dataset is None:
|
|
64
|
+
data.setup()
|
|
65
|
+
subset = data.dataset if split == "all" else getattr(data, f"{split}_dataset")
|
|
66
|
+
if subset is None:
|
|
67
|
+
raise RuntimeError(f"Data module has no '{split}' split available.")
|
|
68
|
+
return build_loader(subset)
|
|
69
|
+
|
|
70
|
+
if isinstance(data, DataLoader):
|
|
71
|
+
if isinstance(data.sampler, RandomSampler):
|
|
72
|
+
warnings.warn(
|
|
73
|
+
f"The supplied DataLoader shuffles, so {verb} rows will not follow "
|
|
74
|
+
"dataset order and cannot be joined to external metadata. Pass "
|
|
75
|
+
"shuffle=False for a reproducible ordering.",
|
|
76
|
+
RuntimeWarning,
|
|
77
|
+
stacklevel=4,
|
|
78
|
+
)
|
|
79
|
+
return data
|
|
80
|
+
|
|
81
|
+
if isinstance(data, Dataset):
|
|
82
|
+
return build_loader(data)
|
|
83
|
+
|
|
84
|
+
if isinstance(data, str | Path):
|
|
85
|
+
array = as_model_input(read_ecg_recording(data), input_channels)
|
|
86
|
+
return build_loader(TensorDataset(torch.as_tensor(array)))
|
|
87
|
+
|
|
88
|
+
if isinstance(data, torch.Tensor):
|
|
89
|
+
array = as_model_input(data.detach().cpu().numpy(), input_channels)
|
|
90
|
+
return build_loader(TensorDataset(torch.as_tensor(array)))
|
|
91
|
+
|
|
92
|
+
if isinstance(data, np.ndarray):
|
|
93
|
+
array = as_model_input(data, input_channels)
|
|
94
|
+
return build_loader(TensorDataset(torch.as_tensor(array)))
|
|
95
|
+
|
|
96
|
+
raise TypeError(
|
|
97
|
+
f"Unsupported input type: {type(data).__name__}. Pass an array, tensor, file path, "
|
|
98
|
+
f"Dataset, DataLoader, or ECGDataModule."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def unpack_batch(batch: Any) -> tuple[torch.Tensor, torch.Tensor | None]:
|
|
103
|
+
"""Split a batch into signals and optional labels.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
batch: A tensor, or a sequence whose first element is the signals
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Tuple of the signals and the labels, which are None when absent.
|
|
110
|
+
"""
|
|
111
|
+
if isinstance(batch, list | tuple):
|
|
112
|
+
signals = batch[0]
|
|
113
|
+
labels = batch[1] if len(batch) > 1 else None
|
|
114
|
+
return signals, labels
|
|
115
|
+
return batch, None
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Shared model-loading machinery for wrapper classes (internal).
|
|
2
|
+
|
|
3
|
+
FeatureExtractor and ECGExplainer both wrap a model they can load from
|
|
4
|
+
registered pretrained weights, and both need to split caller kwargs between
|
|
5
|
+
their own constructor and the model's. That logic lives here once.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import inspect
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from torch import nn
|
|
13
|
+
|
|
14
|
+
from deepecgkit.models.factory import infer_input_channels
|
|
15
|
+
from deepecgkit.models.pretrained import from_pretrained, model_name_for_weights
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"load_pretrained_resolving_name",
|
|
19
|
+
"model_input_channels",
|
|
20
|
+
"split_wrapper_kwargs",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def model_input_channels(model: nn.Module) -> int | None:
|
|
25
|
+
"""Lead count declared by the model, or recovered from its weights.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
model: The wrapped model
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The lead count, or None when unrecoverable.
|
|
32
|
+
"""
|
|
33
|
+
declared = getattr(model, "input_channels", None)
|
|
34
|
+
if isinstance(declared, int):
|
|
35
|
+
return declared
|
|
36
|
+
return infer_input_channels(model.state_dict())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def split_wrapper_kwargs(
|
|
40
|
+
wrapper_cls: type, kwargs: dict[str, Any]
|
|
41
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
42
|
+
"""Split kwargs between a wrapper class's constructor and the model's.
|
|
43
|
+
|
|
44
|
+
The wrapper's accepted names are read from its ``__init__`` signature, so
|
|
45
|
+
adding a constructor parameter never requires updating a hand-maintained
|
|
46
|
+
key list.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
wrapper_cls: The wrapper class (FeatureExtractor, ECGExplainer, ...)
|
|
50
|
+
kwargs: Mixed keyword arguments from the caller
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Tuple of (wrapper kwargs, model kwargs).
|
|
54
|
+
"""
|
|
55
|
+
accepted = set(inspect.signature(wrapper_cls.__init__).parameters) - {
|
|
56
|
+
"self",
|
|
57
|
+
"model",
|
|
58
|
+
"model_name",
|
|
59
|
+
}
|
|
60
|
+
wrapper_kwargs = {k: v for k, v in kwargs.items() if k in accepted}
|
|
61
|
+
model_kwargs = {k: v for k, v in kwargs.items() if k not in accepted}
|
|
62
|
+
return wrapper_kwargs, model_kwargs
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def load_pretrained_resolving_name(
|
|
66
|
+
weights: str | Path,
|
|
67
|
+
model_name: str | None = None,
|
|
68
|
+
**model_kwargs: Any,
|
|
69
|
+
) -> tuple[nn.Module, str]:
|
|
70
|
+
"""Load pretrained weights, resolving the model name from the registry.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
weights: Name of registered pretrained weights, or a path
|
|
74
|
+
model_name: Registered model name; inferred from the weight registry
|
|
75
|
+
when the weights are registered
|
|
76
|
+
**model_kwargs: Constructor arguments forwarded to the model
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Tuple of the loaded model and the resolved registry name.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ValueError: If the model name is neither given nor inferable.
|
|
83
|
+
"""
|
|
84
|
+
resolved = model_name or model_name_for_weights(str(weights))
|
|
85
|
+
if resolved is None:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Could not determine which model '{weights}' belongs to. Pass model_name explicitly."
|
|
88
|
+
)
|
|
89
|
+
model = from_pretrained(resolved, weights, **model_kwargs)
|
|
90
|
+
return model, resolved
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Post-hoc analysis of finished training runs.
|
|
2
|
+
|
|
3
|
+
This subpackage aggregates the metrics written by completed runs, summarises
|
|
4
|
+
them across random seeds, and tests whether cross-dataset ranking reversals
|
|
5
|
+
survive that variance. It is distinct from
|
|
6
|
+
:mod:`deepecgkit.evaluation`, which evaluates a live model.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from deepecgkit.analysis.aggregation import (
|
|
10
|
+
RunRecord,
|
|
11
|
+
aggregate_runs,
|
|
12
|
+
discover_runs,
|
|
13
|
+
read_run_metrics,
|
|
14
|
+
reconcile_averaging,
|
|
15
|
+
runs_to_frame,
|
|
16
|
+
)
|
|
17
|
+
from deepecgkit.analysis.latex import (
|
|
18
|
+
escape_latex,
|
|
19
|
+
flip_survival_table,
|
|
20
|
+
rank_distribution_table,
|
|
21
|
+
results_table,
|
|
22
|
+
tau_table,
|
|
23
|
+
transfer_matrix_table,
|
|
24
|
+
write_tables,
|
|
25
|
+
)
|
|
26
|
+
from deepecgkit.analysis.ranking import (
|
|
27
|
+
all_flip_tests,
|
|
28
|
+
evaluate_rank_claim,
|
|
29
|
+
rank_distribution,
|
|
30
|
+
rank_within_seed,
|
|
31
|
+
tau_summary,
|
|
32
|
+
)
|
|
33
|
+
from deepecgkit.analysis.statistics import (
|
|
34
|
+
benjamini_yekutieli_adjust,
|
|
35
|
+
holm_adjust,
|
|
36
|
+
is_bimodal,
|
|
37
|
+
kendall_tau,
|
|
38
|
+
min_attainable_kendall_p,
|
|
39
|
+
nadeau_bengio_factor,
|
|
40
|
+
paired_summary,
|
|
41
|
+
summarize_scores,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"RunRecord",
|
|
46
|
+
"aggregate_runs",
|
|
47
|
+
"all_flip_tests",
|
|
48
|
+
"benjamini_yekutieli_adjust",
|
|
49
|
+
"discover_runs",
|
|
50
|
+
"escape_latex",
|
|
51
|
+
"evaluate_rank_claim",
|
|
52
|
+
"flip_survival_table",
|
|
53
|
+
"holm_adjust",
|
|
54
|
+
"is_bimodal",
|
|
55
|
+
"kendall_tau",
|
|
56
|
+
"min_attainable_kendall_p",
|
|
57
|
+
"nadeau_bengio_factor",
|
|
58
|
+
"paired_summary",
|
|
59
|
+
"rank_distribution",
|
|
60
|
+
"rank_distribution_table",
|
|
61
|
+
"rank_within_seed",
|
|
62
|
+
"read_run_metrics",
|
|
63
|
+
"reconcile_averaging",
|
|
64
|
+
"results_table",
|
|
65
|
+
"runs_to_frame",
|
|
66
|
+
"summarize_scores",
|
|
67
|
+
"tau_summary",
|
|
68
|
+
"tau_table",
|
|
69
|
+
"transfer_matrix_table",
|
|
70
|
+
"write_tables",
|
|
71
|
+
]
|