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.
Files changed (61) hide show
  1. deepecgkit/__init__.py +42 -0
  2. deepecgkit/__main__.py +6 -0
  3. deepecgkit/cli/__init__.py +61 -0
  4. deepecgkit/cli/commands/__init__.py +16 -0
  5. deepecgkit/cli/commands/evaluate.py +83 -0
  6. deepecgkit/cli/commands/info.py +73 -0
  7. deepecgkit/cli/commands/predict.py +94 -0
  8. deepecgkit/cli/commands/train.py +331 -0
  9. deepecgkit/cli/config.py +23 -0
  10. deepecgkit/cli/evaluation.py +251 -0
  11. deepecgkit/cli/logger.py +26 -0
  12. deepecgkit/cli/main.py +429 -0
  13. deepecgkit/cli/registry.py +32 -0
  14. deepecgkit/datasets/__init__.py +33 -0
  15. deepecgkit/datasets/af_classification.py +301 -0
  16. deepecgkit/datasets/base.py +146 -0
  17. deepecgkit/datasets/ltafdb.py +363 -0
  18. deepecgkit/datasets/mitbih_afdb.py +287 -0
  19. deepecgkit/datasets/modules.py +222 -0
  20. deepecgkit/datasets/preprocessing.py +208 -0
  21. deepecgkit/datasets/ptbxl.py +567 -0
  22. deepecgkit/datasets/splitting.py +225 -0
  23. deepecgkit/datasets/unified_af.py +316 -0
  24. deepecgkit/evaluation/__init__.py +40 -0
  25. deepecgkit/evaluation/evaluator.py +398 -0
  26. deepecgkit/evaluation/metrics.py +164 -0
  27. deepecgkit/evaluation/visualization.py +285 -0
  28. deepecgkit/models/__init__.py +51 -0
  29. deepecgkit/models/af_classifier.py +177 -0
  30. deepecgkit/models/convnext_v2_1d.py +183 -0
  31. deepecgkit/models/crnn.py +117 -0
  32. deepecgkit/models/deep_res_cnn.py +215 -0
  33. deepecgkit/models/ecg_dualnet.py +230 -0
  34. deepecgkit/models/fcn_wang.py +88 -0
  35. deepecgkit/models/gru_model.py +99 -0
  36. deepecgkit/models/inception_time.py +197 -0
  37. deepecgkit/models/kanres_wide_x.py +150 -0
  38. deepecgkit/models/kanres_x.py +126 -0
  39. deepecgkit/models/lstm_model.py +96 -0
  40. deepecgkit/models/mamba1d.py +232 -0
  41. deepecgkit/models/medformer.py +229 -0
  42. deepecgkit/models/resnet1d.py +189 -0
  43. deepecgkit/models/resnet1d_wang.py +182 -0
  44. deepecgkit/models/se_resnet1d.py +232 -0
  45. deepecgkit/models/simple_cnn.py +82 -0
  46. deepecgkit/models/tcn.py +178 -0
  47. deepecgkit/models/transformer_ecg.py +127 -0
  48. deepecgkit/models/xresnet1d.py +265 -0
  49. deepecgkit/models/xresnet1d_benchmark.py +227 -0
  50. deepecgkit/registry.py +97 -0
  51. deepecgkit/training/__init__.py +9 -0
  52. deepecgkit/training/train.py +171 -0
  53. deepecgkit/utils/__init__.py +72 -0
  54. deepecgkit/utils/download.py +123 -0
  55. deepecgkit/utils/weights.py +107 -0
  56. deepecgkit-0.1.0.dist-info/METADATA +99 -0
  57. deepecgkit-0.1.0.dist-info/RECORD +61 -0
  58. deepecgkit-0.1.0.dist-info/WHEEL +5 -0
  59. deepecgkit-0.1.0.dist-info/entry_points.txt +2 -0
  60. deepecgkit-0.1.0.dist-info/licenses/LICENSE +201 -0
  61. deepecgkit-0.1.0.dist-info/top_level.txt +1 -0
deepecgkit/registry.py ADDED
@@ -0,0 +1,97 @@
1
+ """
2
+ Model and dataset registry for deepecg-kit.
3
+
4
+ Provides decorator-based registration so new models and datasets are
5
+ automatically discoverable by the CLI and other consumers.
6
+
7
+ Usage:
8
+ from deepecgkit.registry import register_model, register_dataset
9
+
10
+ @register_model("my-model", description="My custom model")
11
+ class MyModel(nn.Module):
12
+ ...
13
+
14
+ @register_dataset("my-dataset", input_channels=1, description="My dataset")
15
+ class MyDataset(BaseECGDataset):
16
+ ...
17
+ """
18
+
19
+ from typing import Any, Callable, Dict, List, Optional, Type
20
+
21
+ _MODEL_REGISTRY: Dict[str, Dict[str, Any]] = {}
22
+ _DATASET_REGISTRY: Dict[str, Dict[str, Any]] = {}
23
+
24
+
25
+ def register_model(
26
+ name: str,
27
+ description: str = "",
28
+ default_kwargs: Optional[Dict[str, Any]] = None,
29
+ ) -> Callable:
30
+ def decorator(cls: Type) -> Type:
31
+ _MODEL_REGISTRY[name] = {
32
+ "class": cls,
33
+ "description": description,
34
+ "default_kwargs": default_kwargs or {},
35
+ }
36
+ cls._registry_name = name
37
+ return cls
38
+
39
+ return decorator
40
+
41
+
42
+ def register_dataset(
43
+ name: str,
44
+ input_channels: int,
45
+ num_classes: Optional[int] = None,
46
+ description: str = "",
47
+ ) -> Callable:
48
+ def decorator(cls: Type) -> Type:
49
+ _DATASET_REGISTRY[name] = {
50
+ "class": cls,
51
+ "input_channels": input_channels,
52
+ "num_classes": num_classes,
53
+ "description": description,
54
+ }
55
+ cls._registry_name = name
56
+ return cls
57
+
58
+ return decorator
59
+
60
+
61
+ def get_model(name: str) -> Type:
62
+ if name not in _MODEL_REGISTRY:
63
+ available = ", ".join(sorted(_MODEL_REGISTRY.keys()))
64
+ raise KeyError(f"Unknown model: {name}. Available: {available}")
65
+ return _MODEL_REGISTRY[name]["class"]
66
+
67
+
68
+ def get_dataset(name: str) -> Type:
69
+ if name not in _DATASET_REGISTRY:
70
+ available = ", ".join(sorted(_DATASET_REGISTRY.keys()))
71
+ raise KeyError(f"Unknown dataset: {name}. Available: {available}")
72
+ return _DATASET_REGISTRY[name]["class"]
73
+
74
+
75
+ def get_model_names() -> List[str]:
76
+ return sorted(_MODEL_REGISTRY.keys())
77
+
78
+
79
+ def get_dataset_names() -> List[str]:
80
+ return sorted(_DATASET_REGISTRY.keys())
81
+
82
+
83
+ def get_model_info(name: str) -> Dict[str, Any]:
84
+ if name not in _MODEL_REGISTRY:
85
+ raise KeyError(f"Unknown model: {name}")
86
+ return dict(_MODEL_REGISTRY[name])
87
+
88
+
89
+ def get_dataset_info(name: str) -> Dict[str, Any]:
90
+ if name not in _DATASET_REGISTRY:
91
+ raise KeyError(f"Unknown dataset: {name}")
92
+ entry = dict(_DATASET_REGISTRY[name])
93
+ if entry["num_classes"] is None:
94
+ cls = entry["class"]
95
+ if hasattr(cls, "CLASS_LABELS"):
96
+ entry["num_classes"] = len(cls.CLASS_LABELS)
97
+ return entry
@@ -0,0 +1,9 @@
1
+ """
2
+ Training modules for ECG models.
3
+
4
+ This module provides training utilities and Lightning modules for ECG model training.
5
+ """
6
+
7
+ from deepecgkit.training.train import ECGLitModel
8
+
9
+ __all__ = ["ECGLitModel"]
@@ -0,0 +1,171 @@
1
+ import pytorch_lightning as pl
2
+ import torch
3
+
4
+
5
+ class ECGLitModel(pl.LightningModule):
6
+ """
7
+ PyTorch Lightning module for ECG signal classification and regression.
8
+
9
+ This module wraps ECG models and provides training, validation, and testing
10
+ functionality with configurable optimizers and learning rate schedulers.
11
+
12
+ Args:
13
+ model: The ECG model to train (e.g., KanResWideX, AFModel)
14
+ train_config: Dictionary containing training configuration:
15
+ - learning_rate: Learning rate for optimizer
16
+ - scheduler: Dict with 'factor' and 'patience' for ReduceLROnPlateau
17
+ - binary_classification: Bool, if True uses BCE loss for binary tasks
18
+ - multi_label: Bool, if True uses BCE loss for multi-label tasks
19
+ use_plateau_scheduler: If True, uses ReduceLROnPlateau, else StepLR
20
+
21
+ Example:
22
+ >>> model = KanResWideX(input_channels=1, output_size=4)
23
+ >>> config = {
24
+ ... "learning_rate": 0.001,
25
+ ... "scheduler": {"factor": 0.5, "patience": 10},
26
+ ... "binary_classification": False
27
+ ... }
28
+ >>> lit_model = ECGLitModel(model=model, train_config=config)
29
+ """
30
+
31
+ def __init__(self, model, train_config, use_plateau_scheduler=True):
32
+ super().__init__()
33
+ self.save_hyperparameters(ignore=["model"])
34
+
35
+ self.model = model
36
+
37
+ self.learning_rate = train_config["learning_rate"]
38
+ self.scheduler_factor = train_config["scheduler"]["factor"]
39
+ self.scheduler_patience = train_config["scheduler"]["patience"]
40
+
41
+ self.use_plateau_scheduler = use_plateau_scheduler
42
+ self.multi_label = train_config.get("multi_label", False)
43
+
44
+ if self.multi_label or train_config.get("binary_classification", False):
45
+ pos_weight = train_config.get("pos_weight")
46
+ if pos_weight is not None:
47
+ pos_weight = torch.tensor(pos_weight, dtype=torch.float32)
48
+ self.criterion = torch.nn.BCEWithLogitsLoss(pos_weight=pos_weight)
49
+ elif train_config.get("task_type", "classification") == "classification":
50
+ self.criterion = torch.nn.CrossEntropyLoss()
51
+ else:
52
+ self.criterion = torch.nn.MSELoss()
53
+
54
+ self.test_predictions = []
55
+ self.test_targets = []
56
+ self.test_probabilities = []
57
+
58
+ @property
59
+ def _is_binary(self):
60
+ return isinstance(self.criterion, torch.nn.BCEWithLogitsLoss) and not self.multi_label
61
+
62
+ @property
63
+ def _is_classification(self):
64
+ return isinstance(self.criterion, (torch.nn.CrossEntropyLoss, torch.nn.BCEWithLogitsLoss))
65
+
66
+ def _calculate_loss(self, y_hat, y):
67
+ if self.multi_label:
68
+ return self.criterion(y_hat, y.float())
69
+ if self._is_binary:
70
+ return self.criterion(y_hat.squeeze(-1), y.float())
71
+ if isinstance(self.criterion, torch.nn.CrossEntropyLoss):
72
+ return self.criterion(y_hat, y.long())
73
+ return self.criterion(y_hat.float(), y.float())
74
+
75
+ def _compute_acc(self, y_hat, y):
76
+ if self.multi_label:
77
+ preds = (y_hat > 0).float()
78
+ return (preds == y.float()).float().mean()
79
+ if self._is_binary:
80
+ preds = (y_hat.squeeze(-1) > 0).long()
81
+ return (preds == y.long()).float().mean()
82
+ return (torch.argmax(y_hat, dim=1) == y.long()).float().mean()
83
+
84
+ def forward(self, x):
85
+ return self.model(x)
86
+
87
+ def training_step(self, batch, batch_idx):
88
+ x, y = batch
89
+ y_hat = self(x)
90
+ loss = self._calculate_loss(y_hat, y)
91
+ if hasattr(self.model, "l2_regularization_loss"):
92
+ l2_loss = self.model.l2_regularization_loss()
93
+ loss = loss + l2_loss
94
+ self.log("train_loss", loss, prog_bar=True)
95
+ if self._is_classification:
96
+ self.log("train_acc", self._compute_acc(y_hat, y), prog_bar=True)
97
+ return loss
98
+
99
+ def validation_step(self, batch, batch_idx):
100
+ x, y = batch
101
+ y_hat = self(x)
102
+ loss = self._calculate_loss(y_hat, y)
103
+ self.log("val_loss", loss, prog_bar=True)
104
+ if self._is_classification:
105
+ self.log("val_acc", self._compute_acc(y_hat, y), prog_bar=True)
106
+ return loss
107
+
108
+ def on_test_epoch_start(self):
109
+ self.test_predictions = []
110
+ self.test_targets = []
111
+ self.test_probabilities = []
112
+
113
+ def test_step(self, batch, batch_idx):
114
+ x, y = batch
115
+ y_hat = self(x)
116
+ loss = self._calculate_loss(y_hat, y)
117
+ self.log("test_loss", loss, prog_bar=True)
118
+ if self._is_classification:
119
+ if self.multi_label:
120
+ probs = torch.sigmoid(y_hat)
121
+ preds = (probs > 0.5).long()
122
+ acc = (preds == y.long()).float().mean()
123
+ self.test_predictions.append(preds.cpu())
124
+ self.test_targets.append(y.long().cpu())
125
+ self.test_probabilities.append(probs.cpu())
126
+ elif self._is_binary:
127
+ probs_pos = torch.sigmoid(y_hat.squeeze(-1))
128
+ probs = torch.stack([1 - probs_pos, probs_pos], dim=1)
129
+ preds = (probs_pos > 0.5).long()
130
+ acc = (preds == y.long()).float().mean()
131
+ self.test_predictions.append(preds.cpu())
132
+ self.test_targets.append(y.long().cpu())
133
+ self.test_probabilities.append(probs.cpu())
134
+ else:
135
+ probs = torch.softmax(y_hat, dim=1)
136
+ preds = torch.argmax(probs, dim=1)
137
+ acc = (preds == y.long()).float().mean()
138
+ self.test_predictions.append(preds.cpu())
139
+ self.test_targets.append(y.long().cpu())
140
+ self.test_probabilities.append(probs.cpu())
141
+ self.log("test_acc", acc, prog_bar=True)
142
+ return loss
143
+
144
+ def get_test_results(self):
145
+ if not self.test_predictions:
146
+ return None, None, None
147
+ return (
148
+ torch.cat(self.test_predictions).numpy(),
149
+ torch.cat(self.test_targets).numpy(),
150
+ torch.cat(self.test_probabilities).numpy(),
151
+ )
152
+
153
+ def configure_optimizers(self):
154
+ optimizer = torch.optim.Adam(self.parameters(), lr=self.learning_rate)
155
+ if self.use_plateau_scheduler:
156
+ scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
157
+ optimizer,
158
+ mode="min",
159
+ factor=self.scheduler_factor,
160
+ patience=self.scheduler_patience,
161
+ )
162
+ return {
163
+ "optimizer": optimizer,
164
+ "lr_scheduler": {"scheduler": scheduler, "monitor": "val_loss"},
165
+ }
166
+ else:
167
+ scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=1, gamma=0.9)
168
+ return {
169
+ "optimizer": optimizer,
170
+ "lr_scheduler": scheduler,
171
+ }
@@ -0,0 +1,72 @@
1
+ import csv
2
+ import os
3
+ from typing import Dict, List, Optional, Tuple
4
+
5
+ import numpy as np
6
+
7
+ from deepecgkit.utils.weights import (
8
+ list_pretrained_weights,
9
+ load_pretrained_weights,
10
+ register_weights,
11
+ )
12
+
13
+ __all__ = [
14
+ "list_pretrained_weights",
15
+ "load_pretrained_weights",
16
+ "read_csv",
17
+ "register_weights",
18
+ ]
19
+
20
+
21
+ def read_csv(
22
+ csv_file: str,
23
+ delimiter: str = ",",
24
+ transpose: bool = False,
25
+ skip_header: bool = True,
26
+ dtype: Optional[type] = None,
27
+ ) -> Tuple[np.ndarray, Dict[str, int]]:
28
+ """
29
+ Read CSV file and return data array and header mapping.
30
+
31
+ Args:
32
+ csv_file: Path to the CSV file
33
+ delimiter: Column delimiter (default: ',')
34
+ transpose: Whether to transpose the data array
35
+ skip_header: Whether to skip the first row as header
36
+ dtype: Data type for the numpy array
37
+
38
+ Returns:
39
+ Tuple of (data_array, header_mapping)
40
+ """
41
+ if not os.path.exists(csv_file):
42
+ raise FileNotFoundError(f"CSV file not found: {csv_file}")
43
+
44
+ data: List[List[str]] = []
45
+ header: Dict[str, int] = {}
46
+
47
+ try:
48
+ with open(csv_file) as f:
49
+ csv_data = csv.reader(f, delimiter=delimiter)
50
+
51
+ if skip_header:
52
+ try:
53
+ temp = next(csv_data)
54
+ header = {k: v for v, k in enumerate(temp)}
55
+ except StopIteration as err:
56
+ raise ValueError("File is empty") from err
57
+
58
+ for row in csv_data:
59
+ data.append(row)
60
+
61
+ if not data:
62
+ raise ValueError("No data found in CSV file")
63
+
64
+ data_array = np.array(data, dtype=dtype)
65
+
66
+ if transpose:
67
+ data_array = np.transpose(data_array)
68
+
69
+ return data_array, header
70
+
71
+ except Exception as err:
72
+ raise ValueError(f"Error reading CSV file: {err!s}") from err
@@ -0,0 +1,123 @@
1
+ import time
2
+ import urllib.request
3
+ import zipfile
4
+ from pathlib import Path
5
+ from typing import List, Optional, Union
6
+
7
+ from tqdm import tqdm
8
+
9
+
10
+ class DownloadProgressBar(tqdm):
11
+ """Progress bar for downloads."""
12
+
13
+ def update_to(self, b=1, bsize=1, tsize=None):
14
+ if tsize is not None:
15
+ self.total = tsize
16
+ self.update(b * bsize - self.n)
17
+
18
+
19
+ def download_file(
20
+ url: str,
21
+ file_path: Union[str, Path],
22
+ desc: Optional[str] = None,
23
+ force: bool = False,
24
+ max_retries: int = 3,
25
+ ) -> None:
26
+ """Download a file with a progress bar.
27
+
28
+ Args:
29
+ url: URL to download from
30
+ file_path: Path to save the file to
31
+ desc: Description for the progress bar
32
+ force: If True, re-download even if file exists
33
+ max_retries: Maximum number of download attempts
34
+ """
35
+ file_path = Path(file_path)
36
+ file_path.parent.mkdir(parents=True, exist_ok=True)
37
+
38
+ if file_path.exists() and not force:
39
+ if file_path.suffix.lower() == ".zip":
40
+ try:
41
+ with zipfile.ZipFile(file_path, "r") as zip_ref:
42
+ if zip_ref.testzip() is not None:
43
+ print(f"Corrupted zip file detected at {file_path}, re-downloading...")
44
+ file_path.unlink()
45
+ else:
46
+ print(f"File already exists at {file_path}, skipping download.")
47
+ return
48
+ except zipfile.BadZipFile:
49
+ print(f"Corrupted zip file detected at {file_path}, re-downloading...")
50
+ file_path.unlink()
51
+ else:
52
+ print(f"File already exists at {file_path}, skipping download.")
53
+ return
54
+
55
+ last_exception = None
56
+ for attempt in range(max_retries):
57
+ try:
58
+ with DownloadProgressBar(unit="B", unit_scale=True, miniters=1, desc=desc) as t:
59
+ urllib.request.urlretrieve(url, filename=file_path, reporthook=t.update_to)
60
+
61
+ if file_path.suffix.lower() == ".zip":
62
+ with zipfile.ZipFile(file_path, "r") as zip_ref:
63
+ if zip_ref.testzip() is not None:
64
+ raise zipfile.BadZipFile("Downloaded zip file is corrupted")
65
+ return
66
+
67
+ except Exception as e:
68
+ last_exception = e
69
+ if file_path.exists():
70
+ file_path.unlink()
71
+
72
+ if attempt < max_retries - 1:
73
+ wait_time = 2**attempt
74
+ print(f"Download attempt {attempt + 1} failed: {e}")
75
+ print(f"Retrying in {wait_time} seconds...")
76
+ time.sleep(wait_time)
77
+ else:
78
+ raise RuntimeError(
79
+ f"Failed to download {url} after {max_retries} attempts: {last_exception}"
80
+ ) from last_exception
81
+
82
+
83
+ def download_and_extract(
84
+ urls: List[str],
85
+ file_paths: List[Union[str, Path]],
86
+ extract_dir: Optional[Union[str, Path]] = None,
87
+ descs: Optional[List[str]] = None,
88
+ force: bool = False,
89
+ ) -> None:
90
+ """Download files and optionally extract zip files.
91
+
92
+ Args:
93
+ urls: List of URLs to download from
94
+ file_paths: List of paths to save the files to
95
+ extract_dir: Directory to extract zip files to. If None, files are not extracted
96
+ descs: List of descriptions for the progress bars
97
+ force: If True, re-download even if files exist
98
+ """
99
+ if descs is None:
100
+ descs = [f"Downloading file {i + 1}/{len(urls)}" for i in range(len(urls))]
101
+
102
+ for url, file_path_str, desc in zip(urls, file_paths, descs):
103
+ file_path = Path(file_path_str)
104
+ download_file(url, file_path, desc, force=force, max_retries=3)
105
+
106
+ if extract_dir is not None and file_path.suffix.lower() == ".zip":
107
+ extract_dir = Path(extract_dir)
108
+ extracted_marker = extract_dir / f".{file_path.stem}_extracted"
109
+
110
+ if extracted_marker.exists() and not force:
111
+ print(f"{file_path.name} already extracted, skipping...")
112
+ continue
113
+
114
+ print(f"Extracting {file_path.name} to {extract_dir}...")
115
+ try:
116
+ with zipfile.ZipFile(file_path, "r") as zip_ref:
117
+ zip_ref.extractall(extract_dir)
118
+ extracted_marker.touch()
119
+ file_path.unlink()
120
+ except Exception as e:
121
+ if extracted_marker.exists():
122
+ extracted_marker.unlink()
123
+ raise RuntimeError(f"Failed to extract {file_path}: {e}") from e
@@ -0,0 +1,107 @@
1
+ from pathlib import Path
2
+ from typing import Dict, Optional, Union
3
+
4
+ import torch
5
+
6
+ from deepecgkit.utils.download import download_file
7
+
8
+ WEIGHTS_REGISTRY: Dict[str, Dict] = {
9
+ "kanres-af-30s": {
10
+ "url": "https://github.com/your-org/deepecg-kit/releases/download/v1.0.0/kanres-af-30s.pt",
11
+ "model_class": "KanResWideX",
12
+ "model_kwargs": {"input_channels": 1, "output_size": 4, "base_channels": 64},
13
+ "description": "KanResWideX trained on AF classification (30s recordings)",
14
+ },
15
+ "afmodel-30s": {
16
+ "url": "https://github.com/your-org/deepecg-kit/releases/download/v1.0.0/afmodel-30s.pt",
17
+ "model_class": "AFModel",
18
+ "model_kwargs": {"recording_length": 30},
19
+ "description": "AFModel trained on PhysioNet 2017 Challenge (30s recordings)",
20
+ },
21
+ }
22
+
23
+
24
+ def get_weights_dir() -> Path:
25
+ cache_dir = Path.home() / ".cache" / "deepecgkit" / "weights"
26
+ cache_dir.mkdir(parents=True, exist_ok=True)
27
+ return cache_dir
28
+
29
+
30
+ def list_pretrained_weights() -> Dict[str, str]:
31
+ """List all available pretrained weights with descriptions."""
32
+ return {name: info["description"] for name, info in WEIGHTS_REGISTRY.items()}
33
+
34
+
35
+ def get_weight_info(weight_name: str) -> Dict:
36
+ """Get information about a specific pretrained weight."""
37
+ if weight_name not in WEIGHTS_REGISTRY:
38
+ available = ", ".join(WEIGHTS_REGISTRY.keys())
39
+ raise ValueError(f"Unknown weight '{weight_name}'. Available: {available}")
40
+ return WEIGHTS_REGISTRY[weight_name]
41
+
42
+
43
+ def download_weights(weight_name: str, force: bool = False) -> Path:
44
+ """Download pretrained weights by name.
45
+
46
+ Args:
47
+ weight_name: Name of the pretrained weights (e.g., "kanres-af-30s")
48
+ force: If True, re-download even if weights exist locally
49
+
50
+ Returns:
51
+ Path to the downloaded weights file
52
+ """
53
+ info = get_weight_info(weight_name)
54
+ weights_dir = get_weights_dir()
55
+ weight_path = weights_dir / f"{weight_name}.pt"
56
+
57
+ download_file(
58
+ url=info["url"],
59
+ file_path=weight_path,
60
+ desc=f"Downloading {weight_name} weights",
61
+ force=force,
62
+ )
63
+
64
+ return weight_path
65
+
66
+
67
+ def load_pretrained_weights(
68
+ weight_name: str,
69
+ map_location: Optional[Union[str, torch.device]] = None,
70
+ force_download: bool = False,
71
+ ) -> Dict:
72
+ """Load pretrained weights by name.
73
+
74
+ Args:
75
+ weight_name: Name of the pretrained weights
76
+ map_location: Device to map weights to (e.g., "cpu", "cuda")
77
+ force_download: If True, re-download weights even if cached
78
+
79
+ Returns:
80
+ State dict containing model weights
81
+ """
82
+ weight_path = download_weights(weight_name, force=force_download)
83
+ return torch.load(weight_path, map_location=map_location, weights_only=True)
84
+
85
+
86
+ def register_weights(
87
+ name: str,
88
+ url: str,
89
+ model_class: str,
90
+ model_kwargs: Dict,
91
+ description: str = "",
92
+ ) -> None:
93
+ """Register custom pretrained weights.
94
+
95
+ Args:
96
+ name: Unique name for the weights
97
+ url: URL to download weights from
98
+ model_class: Name of the model class these weights are for
99
+ model_kwargs: Keyword arguments to instantiate the model
100
+ description: Human-readable description of the weights
101
+ """
102
+ WEIGHTS_REGISTRY[name] = {
103
+ "url": url,
104
+ "model_class": model_class,
105
+ "model_kwargs": model_kwargs,
106
+ "description": description,
107
+ }
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepecgkit
3
+ Version: 0.1.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: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: numpy>=1.20.0
13
+ Requires-Dist: scipy>=1.7.0
14
+ Requires-Dist: torch>=1.9.0
15
+ Requires-Dist: pandas>=1.3.0
16
+ Requires-Dist: scikit-learn>=0.24.0
17
+ Requires-Dist: matplotlib>=3.4.0
18
+ Requires-Dist: wfdb>=3.4.0
19
+ Requires-Dist: pyEDFlib>=0.1.0
20
+ Requires-Dist: PyWavelets>=1.1.0
21
+ Requires-Dist: seaborn>=0.11.0
22
+ Requires-Dist: pytorch-lightning>=1.5.0
23
+ Requires-Dist: litmodels>=0.1.8
24
+ Requires-Dist: tensorboardx>=2.6.2.2
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest-cov>=3.0.0; extra == "dev"
27
+ Requires-Dist: black>=22.0.0; extra == "dev"
28
+ Requires-Dist: isort>=5.0.0; extra == "dev"
29
+ Requires-Dist: mypy>=0.900; extra == "dev"
30
+ Requires-Dist: ruff>=0.11.13; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # DeepECG-Kit
34
+
35
+ A PyTorch Lightning-based deep learning library for ECG analysis and arrhythmia classification. Provides a CLI for training, evaluation, and inference across multiple model architectures and ECG datasets.
36
+
37
+ Install with `pip install deepecgkit`, or for local development clone the repo and run `pip install -e ".[dev]"`.
38
+
39
+ ## Usage
40
+
41
+ ```bash
42
+ deepecg train -m kanres -d af-classification # train (auto-downloads dataset)
43
+ deepecg evaluate --checkpoint model.ckpt -d af-classification
44
+ deepecg predict --checkpoint model.ckpt --input ecg.npy
45
+ deepecg resume --checkpoint model.ckpt --epochs 100
46
+ deepecg list-models # list available models/datasets
47
+ deepecg list-datasets
48
+ deepecg info -m kanres
49
+ ```
50
+
51
+ **Models:** `afmodel`, `convnext-v2`, `crnn`, `deep-res-cnn`, `dualnet`, `fcn-wang`, `gru`, `inception-time`, `kanres`, `kanres-deep`, `lstm`, `mamba`, `medformer`, `resnet`, `resnet-wang`, `se-resnet`, `simple-cnn`, `tcn`, `transformer`, `xresnet`, `xresnet1d-benchmark`
52
+
53
+ **Datasets:** `af-classification` (1-lead, 4-class, PhysioNet 2017), `ptbxl` (12-lead, 5 superclasses, multi-label), `mitbih-afdb` (2-lead, binary/4-class, MIT-BIH AFDB), `ltafdb` (2-lead, binary/4-class, Long-Term AFDB), `unified-af` (1-lead, 4-class, combined cross-dataset).
54
+
55
+ ## Experiments
56
+
57
+ Experiments are defined in `experiments.yaml` and run via `python scripts/run_experiments.py`. Each experiment trains a model on a dataset using the CLI under the hood. Results save to `runs/{timestamp}-{model}-{dataset}/` with checkpoints (top 3 by val_loss), training plots, and test evaluation (confusion matrix, classification report).
58
+
59
+ ### Default Training Parameters
60
+
61
+ | Parameter | Value |
62
+ |-----------|-------|
63
+ | Epochs | 50 |
64
+ | Batch size | 32 |
65
+ | Learning rate | 0.001 |
66
+ | Optimizer | Adam |
67
+ | LR scheduler | ReduceLROnPlateau (factor=0.5, patience=5) |
68
+ | Early stopping patience | 10 |
69
+ | Validation split | 0.2 |
70
+ | Test split | 0.1 |
71
+ | Seed | 42 |
72
+
73
+ ## References
74
+
75
+ ### Datasets
76
+
77
+ 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/)
78
+
79
+ 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/)
80
+
81
+ 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/)
82
+
83
+ 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/)
84
+
85
+ 5. **unified-af** — Combined cross-dataset using af-classification, mitbih-afdb, and ltafdb (see references above).
86
+
87
+ ### Models
88
+
89
+ 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)
90
+
91
+ 7. **deep-res-cnn** — Elyamani H. "ECG Classification Using Deep Residual CNN." 2022. [[GitHub]](https://github.com/HaneenElyamani/ECG-classification)
92
+
93
+ 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)
94
+
95
+ 9. **mamba** — Gu A, Dao T. "Mamba: Linear-Time Sequence Modeling with Selective State Spaces." *arXiv:2312.00752*, 2023. [[GitHub]](https://github.com/state-spaces/mamba)
96
+
97
+ 10. **medformer** — Wang N, Liang X, Wang Z, Zhao J, Liu Y, Peng L, Miao C. "MedFormer: A Multi-Granularity Patching Transformer for Medical Time-Series Classification." *NeurIPS*, 2024. [[GitHub]](https://github.com/DL4mHealth/Medformer)
98
+
99
+ 11. **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)