deepecgkit 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.
- deepecgkit/__init__.py +42 -0
- deepecgkit/__main__.py +6 -0
- deepecgkit/cli/__init__.py +61 -0
- deepecgkit/cli/commands/__init__.py +16 -0
- deepecgkit/cli/commands/evaluate.py +83 -0
- deepecgkit/cli/commands/info.py +73 -0
- deepecgkit/cli/commands/predict.py +94 -0
- deepecgkit/cli/commands/train.py +331 -0
- deepecgkit/cli/config.py +23 -0
- deepecgkit/cli/evaluation.py +251 -0
- deepecgkit/cli/logger.py +26 -0
- deepecgkit/cli/main.py +429 -0
- deepecgkit/cli/registry.py +32 -0
- deepecgkit/datasets/__init__.py +33 -0
- deepecgkit/datasets/af_classification.py +301 -0
- deepecgkit/datasets/base.py +146 -0
- deepecgkit/datasets/ltafdb.py +363 -0
- deepecgkit/datasets/mitbih_afdb.py +287 -0
- deepecgkit/datasets/modules.py +222 -0
- deepecgkit/datasets/preprocessing.py +208 -0
- deepecgkit/datasets/ptbxl.py +567 -0
- deepecgkit/datasets/splitting.py +225 -0
- deepecgkit/datasets/unified_af.py +316 -0
- deepecgkit/evaluation/__init__.py +40 -0
- deepecgkit/evaluation/evaluator.py +398 -0
- deepecgkit/evaluation/metrics.py +164 -0
- deepecgkit/evaluation/visualization.py +285 -0
- deepecgkit/models/__init__.py +51 -0
- deepecgkit/models/af_classifier.py +177 -0
- deepecgkit/models/convnext_v2_1d.py +183 -0
- deepecgkit/models/crnn.py +117 -0
- deepecgkit/models/deep_res_cnn.py +215 -0
- deepecgkit/models/ecg_dualnet.py +230 -0
- deepecgkit/models/fcn_wang.py +88 -0
- deepecgkit/models/gru_model.py +99 -0
- deepecgkit/models/inception_time.py +197 -0
- deepecgkit/models/kanres_wide_x.py +150 -0
- deepecgkit/models/kanres_x.py +126 -0
- deepecgkit/models/lstm_model.py +96 -0
- deepecgkit/models/mamba1d.py +232 -0
- deepecgkit/models/medformer.py +229 -0
- deepecgkit/models/resnet1d.py +189 -0
- deepecgkit/models/resnet1d_wang.py +182 -0
- deepecgkit/models/se_resnet1d.py +232 -0
- deepecgkit/models/simple_cnn.py +82 -0
- deepecgkit/models/tcn.py +178 -0
- deepecgkit/models/transformer_ecg.py +127 -0
- deepecgkit/models/xresnet1d.py +265 -0
- deepecgkit/models/xresnet1d_benchmark.py +227 -0
- deepecgkit/registry.py +97 -0
- deepecgkit/training/__init__.py +9 -0
- deepecgkit/training/train.py +171 -0
- deepecgkit/utils/__init__.py +72 -0
- deepecgkit/utils/download.py +123 -0
- deepecgkit/utils/weights.py +107 -0
- deepecgkit-0.1.0.dist-info/METADATA +99 -0
- deepecgkit-0.1.0.dist-info/RECORD +61 -0
- deepecgkit-0.1.0.dist-info/WHEEL +5 -0
- deepecgkit-0.1.0.dist-info/entry_points.txt +2 -0
- deepecgkit-0.1.0.dist-info/licenses/LICENSE +201 -0
- deepecgkit-0.1.0.dist-info/top_level.txt +1 -0
deepecgkit/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DeepECG-Kit: A comprehensive Python package for ECG analysis using deep learning.
|
|
3
|
+
|
|
4
|
+
This package provides tools for:
|
|
5
|
+
- Loading and preprocessing ECG data
|
|
6
|
+
- Training deep learning models on ECG signals
|
|
7
|
+
- Evaluating model performance
|
|
8
|
+
- Traditional ECG analysis methods
|
|
9
|
+
- Visualization and reporting
|
|
10
|
+
|
|
11
|
+
Basic usage:
|
|
12
|
+
import deepecgkit as ecg
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
data = ecg.load_ecg_data("path/to/data")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
preprocessed = ecg.preprocess(data)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
model = ecg.models.KanResWideX(input_channels=12, output_size=1)
|
|
22
|
+
trainer = ecg.train_model(model, preprocessed)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
results = ecg.evaluate_model(model, test_data)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from deepecgkit.datasets import ECGDataModule
|
|
29
|
+
from deepecgkit.models import KanResWideX
|
|
30
|
+
from deepecgkit.training import ECGLitModel
|
|
31
|
+
from deepecgkit.utils import read_csv
|
|
32
|
+
|
|
33
|
+
__version__ = "0.1.0"
|
|
34
|
+
__author__ = "DeepECG Kit Contributors"
|
|
35
|
+
__license__ = "MIT"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"ECGDataModule",
|
|
39
|
+
"ECGLitModel",
|
|
40
|
+
"KanResWideX",
|
|
41
|
+
"read_csv",
|
|
42
|
+
]
|
deepecgkit/__main__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for deepecg-kit.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
deepecg train --model <model> --dataset <dataset> [options]
|
|
6
|
+
deepecg evaluate --checkpoint <path> --dataset <dataset> [options]
|
|
7
|
+
deepecg predict --checkpoint <path> --input <file> [options]
|
|
8
|
+
deepecg resume --checkpoint <path> [options]
|
|
9
|
+
deepecg list-models
|
|
10
|
+
deepecg list-datasets
|
|
11
|
+
deepecg info --model <model>
|
|
12
|
+
|
|
13
|
+
Datasets are automatically downloaded when first used if not already present.
|
|
14
|
+
Use --force-download to re-download an existing dataset.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from deepecgkit.cli.commands import (
|
|
18
|
+
evaluate,
|
|
19
|
+
list_datasets,
|
|
20
|
+
list_models,
|
|
21
|
+
predict,
|
|
22
|
+
resume,
|
|
23
|
+
show_info,
|
|
24
|
+
train,
|
|
25
|
+
)
|
|
26
|
+
from deepecgkit.cli.config import load_config
|
|
27
|
+
from deepecgkit.cli.logger import CLILogger
|
|
28
|
+
from deepecgkit.cli.main import main
|
|
29
|
+
from deepecgkit.cli.registry import (
|
|
30
|
+
DATASET_INPUT_CHANNELS,
|
|
31
|
+
DATASET_NAMES,
|
|
32
|
+
DATASET_NUM_CLASSES,
|
|
33
|
+
MODEL_DESCRIPTIONS,
|
|
34
|
+
MODEL_NAMES,
|
|
35
|
+
MODEL_WEIGHTS,
|
|
36
|
+
_get_dataset_registry,
|
|
37
|
+
_get_model_registry,
|
|
38
|
+
)
|
|
39
|
+
from deepecgkit.training import ECGLitModel
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"DATASET_INPUT_CHANNELS",
|
|
43
|
+
"DATASET_NAMES",
|
|
44
|
+
"DATASET_NUM_CLASSES",
|
|
45
|
+
"MODEL_DESCRIPTIONS",
|
|
46
|
+
"MODEL_NAMES",
|
|
47
|
+
"MODEL_WEIGHTS",
|
|
48
|
+
"CLILogger",
|
|
49
|
+
"ECGLitModel",
|
|
50
|
+
"_get_dataset_registry",
|
|
51
|
+
"_get_model_registry",
|
|
52
|
+
"evaluate",
|
|
53
|
+
"list_datasets",
|
|
54
|
+
"list_models",
|
|
55
|
+
"load_config",
|
|
56
|
+
"main",
|
|
57
|
+
"predict",
|
|
58
|
+
"resume",
|
|
59
|
+
"show_info",
|
|
60
|
+
"train",
|
|
61
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""CLI command functions."""
|
|
2
|
+
|
|
3
|
+
from deepecgkit.cli.commands.evaluate import evaluate
|
|
4
|
+
from deepecgkit.cli.commands.info import list_datasets, list_models, show_info
|
|
5
|
+
from deepecgkit.cli.commands.predict import predict
|
|
6
|
+
from deepecgkit.cli.commands.train import resume, train
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"evaluate",
|
|
10
|
+
"list_datasets",
|
|
11
|
+
"list_models",
|
|
12
|
+
"predict",
|
|
13
|
+
"resume",
|
|
14
|
+
"show_info",
|
|
15
|
+
"train",
|
|
16
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Evaluate command."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
import pytorch_lightning as pl
|
|
7
|
+
|
|
8
|
+
from deepecgkit.cli.logger import CLILogger
|
|
9
|
+
from deepecgkit.cli.registry import DATASET_NAMES, _get_dataset_registry
|
|
10
|
+
from deepecgkit.datasets import ECGDataModule
|
|
11
|
+
from deepecgkit.training import ECGLitModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def evaluate(
|
|
15
|
+
checkpoint: str,
|
|
16
|
+
dataset_name: str,
|
|
17
|
+
data_dir: Optional[str] = None,
|
|
18
|
+
batch_size: int = 32,
|
|
19
|
+
num_workers: int = 4,
|
|
20
|
+
accelerator: str = "auto",
|
|
21
|
+
devices: int = 1,
|
|
22
|
+
force_download: bool = False,
|
|
23
|
+
split: str = "test",
|
|
24
|
+
logger: Optional[CLILogger] = None,
|
|
25
|
+
) -> int:
|
|
26
|
+
"""Evaluate a trained model on a dataset."""
|
|
27
|
+
logger = logger or CLILogger()
|
|
28
|
+
|
|
29
|
+
checkpoint_path = Path(checkpoint)
|
|
30
|
+
if not checkpoint_path.exists():
|
|
31
|
+
logger.error(f"Checkpoint not found: {checkpoint}")
|
|
32
|
+
return 1
|
|
33
|
+
|
|
34
|
+
dataset_registry = _get_dataset_registry()
|
|
35
|
+
|
|
36
|
+
if dataset_name not in dataset_registry:
|
|
37
|
+
logger.error(f"Unknown dataset: {dataset_name}")
|
|
38
|
+
logger.info(f"Available datasets: {', '.join(DATASET_NAMES)}")
|
|
39
|
+
return 1
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
logger.info(f"Loading model from {checkpoint}...")
|
|
43
|
+
lit_model = ECGLitModel.load_from_checkpoint(checkpoint)
|
|
44
|
+
|
|
45
|
+
dataset_class = dataset_registry[dataset_name]
|
|
46
|
+
dataset_kwargs: Dict[str, Any] = {"force_download": force_download}
|
|
47
|
+
if data_dir:
|
|
48
|
+
dataset_kwargs["data_dir"] = data_dir
|
|
49
|
+
|
|
50
|
+
logger.info(f"Creating data module with {dataset_name} dataset...")
|
|
51
|
+
data_module = ECGDataModule(
|
|
52
|
+
dataset_class=dataset_class,
|
|
53
|
+
batch_size=batch_size,
|
|
54
|
+
num_workers=num_workers,
|
|
55
|
+
download=True,
|
|
56
|
+
dataset_kwargs=dataset_kwargs,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
trainer = pl.Trainer(
|
|
60
|
+
accelerator=accelerator,
|
|
61
|
+
devices=devices,
|
|
62
|
+
enable_progress_bar=not logger.quiet,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
logger.info(f"Running evaluation on {split} split...")
|
|
66
|
+
if split == "test":
|
|
67
|
+
results = trainer.test(lit_model, data_module)
|
|
68
|
+
elif split == "val":
|
|
69
|
+
results = trainer.validate(lit_model, data_module)
|
|
70
|
+
else:
|
|
71
|
+
logger.error(f"Invalid split: {split}. Use 'test' or 'val'.")
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
if results:
|
|
75
|
+
logger.info("\nEvaluation Results:")
|
|
76
|
+
logger.info("-" * 40)
|
|
77
|
+
for key, value in results[0].items():
|
|
78
|
+
logger.info(f" {key}: {value:.4f}")
|
|
79
|
+
|
|
80
|
+
return 0
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"Evaluation failed: {e}")
|
|
83
|
+
return 1
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Info and listing commands."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from deepecgkit.cli.logger import CLILogger
|
|
6
|
+
from deepecgkit.cli.registry import (
|
|
7
|
+
DATASET_NAMES,
|
|
8
|
+
DATASET_NUM_CLASSES,
|
|
9
|
+
MODEL_DESCRIPTIONS,
|
|
10
|
+
MODEL_NAMES,
|
|
11
|
+
MODEL_WEIGHTS,
|
|
12
|
+
_get_model_registry,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def list_models() -> None:
|
|
17
|
+
"""List all available models."""
|
|
18
|
+
print("Available models:")
|
|
19
|
+
print("-" * 60)
|
|
20
|
+
for name in sorted(MODEL_NAMES):
|
|
21
|
+
desc = MODEL_DESCRIPTIONS.get(name, "")
|
|
22
|
+
weights = MODEL_WEIGHTS.get(name, [])
|
|
23
|
+
print(f" {name}")
|
|
24
|
+
if desc:
|
|
25
|
+
print(f" Description: {desc}")
|
|
26
|
+
if weights:
|
|
27
|
+
print(f" Pretrained weights: {', '.join(weights)}")
|
|
28
|
+
print()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def list_datasets() -> None:
|
|
32
|
+
"""List all available datasets."""
|
|
33
|
+
print("Available datasets:")
|
|
34
|
+
print("-" * 60)
|
|
35
|
+
for name in sorted(DATASET_NAMES):
|
|
36
|
+
num_classes = DATASET_NUM_CLASSES[name]
|
|
37
|
+
print(f" {name}")
|
|
38
|
+
print(f" Classes: {num_classes}")
|
|
39
|
+
print()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def show_info(model_name: str, logger: Optional[CLILogger] = None) -> int:
|
|
43
|
+
"""Show detailed information about a model."""
|
|
44
|
+
logger = logger or CLILogger()
|
|
45
|
+
|
|
46
|
+
if model_name not in MODEL_NAMES:
|
|
47
|
+
logger.error(f"Unknown model: {model_name}")
|
|
48
|
+
logger.info(f"Available models: {', '.join(MODEL_NAMES)}")
|
|
49
|
+
return 1
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
model_registry = _get_model_registry()
|
|
53
|
+
model_class = model_registry[model_name]
|
|
54
|
+
model = model_class(input_channels=1, output_size=4)
|
|
55
|
+
|
|
56
|
+
total_params = sum(p.numel() for p in model.parameters())
|
|
57
|
+
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
|
58
|
+
|
|
59
|
+
print(f"Model: {model_name}")
|
|
60
|
+
print("=" * 60)
|
|
61
|
+
print(f"Description: {MODEL_DESCRIPTIONS.get(model_name, 'N/A')}")
|
|
62
|
+
print(f"Total parameters: {total_params:,}")
|
|
63
|
+
print(f"Trainable parameters: {trainable_params:,}")
|
|
64
|
+
print(f"Pretrained weights: {', '.join(MODEL_WEIGHTS.get(model_name, ['None']))}")
|
|
65
|
+
print()
|
|
66
|
+
print("Architecture:")
|
|
67
|
+
print("-" * 60)
|
|
68
|
+
print(model)
|
|
69
|
+
|
|
70
|
+
return 0
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error(f"Failed to load model info: {e}")
|
|
73
|
+
return 1
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Predict command."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import torch
|
|
10
|
+
import wfdb
|
|
11
|
+
|
|
12
|
+
import deepecgkit.cli as _cli
|
|
13
|
+
from deepecgkit.cli.logger import CLILogger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def predict(
|
|
17
|
+
checkpoint: str,
|
|
18
|
+
input_path: str,
|
|
19
|
+
output_path: Optional[str] = None,
|
|
20
|
+
batch_size: int = 1,
|
|
21
|
+
accelerator: str = "auto",
|
|
22
|
+
logger: Optional[CLILogger] = None,
|
|
23
|
+
) -> int:
|
|
24
|
+
"""Run inference on ECG data."""
|
|
25
|
+
logger = logger or CLILogger()
|
|
26
|
+
|
|
27
|
+
checkpoint_file = Path(checkpoint)
|
|
28
|
+
if not checkpoint_file.exists():
|
|
29
|
+
logger.error(f"Checkpoint not found: {checkpoint}")
|
|
30
|
+
return 1
|
|
31
|
+
|
|
32
|
+
input_file = Path(input_path)
|
|
33
|
+
if not input_file.exists():
|
|
34
|
+
logger.error(f"Input file not found: {input_path}")
|
|
35
|
+
return 1
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
logger.info(f"Loading model from {checkpoint}...")
|
|
39
|
+
lit_model = _cli.ECGLitModel.load_from_checkpoint(checkpoint)
|
|
40
|
+
lit_model.eval()
|
|
41
|
+
|
|
42
|
+
logger.info(f"Loading input data from {input_path}...")
|
|
43
|
+
if input_file.suffix == ".npy":
|
|
44
|
+
data = np.load(input_path)
|
|
45
|
+
elif input_file.suffix == ".csv":
|
|
46
|
+
data = pd.read_csv(input_path).values
|
|
47
|
+
elif input_file.suffix in (".dat", ".hea"):
|
|
48
|
+
record = wfdb.rdrecord(str(input_file.with_suffix("")))
|
|
49
|
+
data = record.p_signal
|
|
50
|
+
else:
|
|
51
|
+
logger.error(f"Unsupported input format: {input_file.suffix}")
|
|
52
|
+
logger.info("Supported formats: .npy, .csv, .dat/.hea (WFDB)")
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
if data.ndim == 1:
|
|
56
|
+
data = data.reshape(1, 1, -1)
|
|
57
|
+
elif data.ndim == 2:
|
|
58
|
+
data = data.reshape(data.shape[0], 1, -1)
|
|
59
|
+
|
|
60
|
+
tensor = torch.tensor(data, dtype=torch.float32)
|
|
61
|
+
|
|
62
|
+
logger.info("Running inference...")
|
|
63
|
+
device = "cuda" if accelerator == "gpu" and torch.cuda.is_available() else "cpu"
|
|
64
|
+
lit_model = lit_model.to(device)
|
|
65
|
+
tensor = tensor.to(device)
|
|
66
|
+
|
|
67
|
+
with torch.no_grad():
|
|
68
|
+
outputs = lit_model(tensor)
|
|
69
|
+
if hasattr(outputs, "logits"):
|
|
70
|
+
outputs = outputs.logits
|
|
71
|
+
probabilities = torch.softmax(outputs, dim=-1)
|
|
72
|
+
predictions = torch.argmax(probabilities, dim=-1)
|
|
73
|
+
|
|
74
|
+
results = {
|
|
75
|
+
"predictions": predictions.cpu().numpy().tolist(),
|
|
76
|
+
"probabilities": probabilities.cpu().numpy().tolist(),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if output_path:
|
|
80
|
+
output_file = Path(output_path)
|
|
81
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
with open(output_file, "w") as f:
|
|
83
|
+
json.dump(results, f, indent=2)
|
|
84
|
+
logger.info(f"Results saved to {output_path}")
|
|
85
|
+
else:
|
|
86
|
+
logger.info("\nPrediction Results:")
|
|
87
|
+
logger.info("-" * 40)
|
|
88
|
+
for i, (pred, prob) in enumerate(zip(results["predictions"], results["probabilities"])):
|
|
89
|
+
logger.info(f" Sample {i}: class={pred}, confidence={max(prob):.4f}")
|
|
90
|
+
|
|
91
|
+
return 0
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(f"Prediction failed: {e}")
|
|
94
|
+
return 1
|