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
@@ -0,0 +1,99 @@
1
+ import torch
2
+ from torch import nn
3
+
4
+ from deepecgkit.registry import register_model
5
+
6
+
7
+ @register_model(
8
+ name="gru",
9
+ description="GRU-based model for sequential ECG analysis",
10
+ )
11
+ class GRUECG(nn.Module):
12
+ """
13
+ GRU-based model for ECG signal classification.
14
+
15
+ A recurrent neural network using GRU layers with concat pooling
16
+ (adaptive avg + adaptive max + last hidden state) for ECG classification.
17
+ Based on the RNN architecture from the PTB-XL benchmark.
18
+
19
+ Reference:
20
+ https://github.com/helme/ecg_ptbxl_benchmarking
21
+
22
+ Args:
23
+ input_channels: Number of input channels (default: 12 for 12-lead ECG)
24
+ output_size: Number of output classes
25
+ hidden_size: Size of GRU hidden state (default: 256)
26
+ num_layers: Number of GRU layers (default: 2)
27
+ dropout_rate: Dropout probability (default: 0.3)
28
+ bidirectional: Use bidirectional GRU (default: False)
29
+
30
+ Example:
31
+ >>> model = GRUECG(input_channels=12, output_size=5)
32
+ >>> x = torch.randn(32, 12, 1000)
33
+ >>> output = model(x)
34
+ >>> print(output.shape)
35
+
36
+ >>> features = model.extract_features(x)
37
+ >>> print(features.shape) # (32, 768) with hidden_size=256, unidirectional
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ input_channels: int = 12,
43
+ output_size: int = 5,
44
+ hidden_size: int = 256,
45
+ num_layers: int = 2,
46
+ dropout_rate: float = 0.3,
47
+ bidirectional: bool = False,
48
+ ):
49
+ super().__init__()
50
+
51
+ self.hidden_size = hidden_size
52
+ self.num_layers = num_layers
53
+ self.bidirectional = bidirectional
54
+ self.num_directions = 2 if bidirectional else 1
55
+
56
+ self.gru = nn.GRU(
57
+ input_size=input_channels,
58
+ hidden_size=hidden_size,
59
+ num_layers=num_layers,
60
+ batch_first=True,
61
+ dropout=dropout_rate if num_layers > 1 else 0,
62
+ bidirectional=bidirectional,
63
+ )
64
+
65
+ rnn_out_dim = hidden_size * self.num_directions
66
+ self._feature_dim = rnn_out_dim * 3
67
+
68
+ self.head = nn.Sequential(
69
+ nn.BatchNorm1d(self._feature_dim),
70
+ nn.Dropout(dropout_rate / 2),
71
+ nn.Linear(self._feature_dim, output_size),
72
+ )
73
+
74
+ @property
75
+ def feature_dim(self) -> int:
76
+ return self._feature_dim
77
+
78
+ def _concat_pool(self, rnn_output: torch.Tensor) -> torch.Tensor:
79
+ x = rnn_output.transpose(1, 2)
80
+ avg_pool = torch.mean(x, dim=2)
81
+ max_pool, _ = torch.max(x, dim=2)
82
+
83
+ if self.bidirectional:
84
+ last = torch.cat(
85
+ [rnn_output[:, -1, : self.hidden_size], rnn_output[:, 0, self.hidden_size :]], dim=1
86
+ )
87
+ else:
88
+ last = rnn_output[:, -1, :]
89
+
90
+ return torch.cat([avg_pool, max_pool, last], dim=1)
91
+
92
+ def extract_features(self, x: torch.Tensor) -> torch.Tensor:
93
+ x = x.transpose(1, 2)
94
+ output, _ = self.gru(x)
95
+ return self._concat_pool(output)
96
+
97
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
98
+ x = self.extract_features(x)
99
+ return self.head(x)
@@ -0,0 +1,197 @@
1
+ import torch
2
+ import torch.nn.functional as func
3
+ from torch import nn
4
+
5
+ from deepecgkit.registry import register_model
6
+
7
+
8
+ class InceptionBlock1D(nn.Module):
9
+ """
10
+ Single Inception block with multi-scale 1D convolutions.
11
+
12
+ Applies parallel convolutions with different kernel sizes to capture
13
+ temporal patterns at multiple scales, plus a max-pool branch.
14
+
15
+ Args:
16
+ in_channels: Number of input channels
17
+ n_filters: Number of filters per branch (default: 32)
18
+ kernel_sizes: Tuple of kernel sizes for parallel branches
19
+ bottleneck_channels: Channels in bottleneck layer (default: 32)
20
+ use_bottleneck: Whether to use 1x1 bottleneck before convolutions
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ in_channels: int,
26
+ n_filters: int = 32,
27
+ kernel_sizes: tuple[int, ...] = (5, 15, 41),
28
+ bottleneck_channels: int = 32,
29
+ use_bottleneck: bool = True,
30
+ ):
31
+ super().__init__()
32
+
33
+ self.use_bottleneck = use_bottleneck
34
+
35
+ if use_bottleneck:
36
+ self.bottleneck = nn.Conv1d(in_channels, bottleneck_channels, kernel_size=1, bias=False)
37
+ conv_in = bottleneck_channels
38
+ else:
39
+ conv_in = in_channels
40
+
41
+ self.conv_branches = nn.ModuleList(
42
+ [
43
+ nn.Conv1d(
44
+ conv_in,
45
+ n_filters,
46
+ kernel_size=ks,
47
+ padding=ks // 2,
48
+ bias=False,
49
+ )
50
+ for ks in kernel_sizes
51
+ ]
52
+ )
53
+
54
+ self.maxpool_branch = nn.Sequential(
55
+ nn.MaxPool1d(kernel_size=3, stride=1, padding=1),
56
+ nn.Conv1d(in_channels, n_filters, kernel_size=1, bias=False),
57
+ )
58
+
59
+ total_filters = n_filters * (len(kernel_sizes) + 1)
60
+ self.bn = nn.BatchNorm1d(total_filters)
61
+
62
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
63
+ if self.use_bottleneck:
64
+ bottleneck_out = self.bottleneck(x)
65
+ conv_outputs = [branch(bottleneck_out) for branch in self.conv_branches]
66
+ else:
67
+ conv_outputs = [branch(x) for branch in self.conv_branches]
68
+
69
+ pool_output = self.maxpool_branch(x)
70
+ conv_outputs.append(pool_output)
71
+
72
+ out = torch.cat(conv_outputs, dim=1)
73
+ out = self.bn(out)
74
+ out = func.relu(out)
75
+ return out
76
+
77
+
78
+ class InceptionResidualBlock1D(nn.Module):
79
+ """
80
+ Inception block with a residual shortcut connection.
81
+
82
+ Args:
83
+ in_channels: Number of input channels
84
+ n_filters: Number of filters per Inception branch
85
+ kernel_sizes: Tuple of kernel sizes for parallel branches
86
+ bottleneck_channels: Channels in bottleneck layer
87
+ """
88
+
89
+ def __init__(
90
+ self,
91
+ in_channels: int,
92
+ n_filters: int = 32,
93
+ kernel_sizes: tuple[int, ...] = (5, 15, 41),
94
+ bottleneck_channels: int = 32,
95
+ ):
96
+ super().__init__()
97
+
98
+ self.inception = InceptionBlock1D(
99
+ in_channels=in_channels,
100
+ n_filters=n_filters,
101
+ kernel_sizes=kernel_sizes,
102
+ bottleneck_channels=bottleneck_channels,
103
+ )
104
+
105
+ out_channels = n_filters * (len(kernel_sizes) + 1)
106
+
107
+ self.shortcut = nn.Sequential(
108
+ nn.Conv1d(in_channels, out_channels, kernel_size=1, bias=False),
109
+ nn.BatchNorm1d(out_channels),
110
+ )
111
+
112
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
113
+ out = self.inception(x)
114
+ shortcut = self.shortcut(x)
115
+ return func.relu(out + shortcut)
116
+
117
+
118
+ @register_model(
119
+ name="inception-time",
120
+ description="Multi-scale temporal CNN inspired by InceptionTime",
121
+ )
122
+ class InceptionTime1D(nn.Module):
123
+ """
124
+ InceptionTime model adapted for 1D ECG signal classification.
125
+
126
+ Uses parallel convolutions at multiple temporal scales (short/medium/long
127
+ kernels) with residual connections to capture both rapid arrhythmia spikes
128
+ and slow rhythm patterns simultaneously.
129
+
130
+ Args:
131
+ input_channels: Number of input channels (default: 1 for single-lead ECG)
132
+ output_size: Number of output classes
133
+ n_filters: Number of filters per Inception branch (default: 32)
134
+ depth: Number of Inception residual blocks (default: 6)
135
+ kernel_sizes: Tuple of kernel sizes for multi-scale branches
136
+ bottleneck_channels: Channels in bottleneck layers (default: 32)
137
+ dropout_rate: Dropout probability (default: 0.3)
138
+
139
+ Example:
140
+ >>> model = InceptionTime1D(input_channels=1, output_size=4)
141
+ >>> x = torch.randn(32, 1, 3000)
142
+ >>> output = model(x)
143
+ >>> print(output.shape)
144
+
145
+ >>> features = model.extract_features(x)
146
+ >>> print(features.shape) # (32, 128) with n_filters=32
147
+ """
148
+
149
+ def __init__(
150
+ self,
151
+ input_channels: int = 1,
152
+ output_size: int = 4,
153
+ n_filters: int = 32,
154
+ depth: int = 6,
155
+ kernel_sizes: tuple[int, ...] = (5, 15, 41),
156
+ bottleneck_channels: int = 32,
157
+ dropout_rate: float = 0.3,
158
+ ):
159
+ super().__init__()
160
+
161
+ n_branches = len(kernel_sizes) + 1
162
+ block_out_channels = n_filters * n_branches
163
+
164
+ blocks = []
165
+ for i in range(depth):
166
+ in_ch = input_channels if i == 0 else block_out_channels
167
+ blocks.append(
168
+ InceptionResidualBlock1D(
169
+ in_channels=in_ch,
170
+ n_filters=n_filters,
171
+ kernel_sizes=kernel_sizes,
172
+ bottleneck_channels=bottleneck_channels,
173
+ )
174
+ )
175
+
176
+ self.blocks = nn.Sequential(*blocks)
177
+ self.global_pool = nn.AdaptiveAvgPool1d(1)
178
+ self.dropout = nn.Dropout(dropout_rate)
179
+ self._feature_dim = block_out_channels
180
+
181
+ self.classifier = nn.Linear(block_out_channels, output_size)
182
+
183
+ @property
184
+ def feature_dim(self) -> int:
185
+ return self._feature_dim
186
+
187
+ def extract_features(self, x: torch.Tensor) -> torch.Tensor:
188
+ x = self.blocks(x)
189
+ x = self.global_pool(x)
190
+ x = torch.flatten(x, 1)
191
+ return x
192
+
193
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
194
+ x = self.extract_features(x)
195
+ x = self.dropout(x)
196
+ x = self.classifier(x)
197
+ return x
@@ -0,0 +1,150 @@
1
+ import torch
2
+ from torch import nn
3
+
4
+ from deepecgkit.registry import register_model
5
+
6
+
7
+ class KanResInit(nn.Module):
8
+ """Initial convolutional block for KanResWideX architecture.
9
+
10
+ Args:
11
+ in_channels: Number of input channels
12
+ filterno_1: Number of filters in first convolution
13
+ filterno_2: Number of filters in second convolution
14
+ filtersize_1: Kernel size for first convolution
15
+ filtersize_2: Kernel size for second convolution
16
+ stride: Stride for first convolution
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ in_channels: int,
22
+ filterno_1: int,
23
+ filterno_2: int,
24
+ filtersize_1: int,
25
+ filtersize_2: int,
26
+ stride: int,
27
+ ):
28
+ super().__init__()
29
+ self.conv1 = nn.Conv1d(in_channels, filterno_1, filtersize_1, stride=stride)
30
+ self.bn1 = nn.BatchNorm1d(filterno_1)
31
+ self.relu1 = nn.ReLU()
32
+ self.conv2 = nn.Conv1d(filterno_1, filterno_2, filtersize_2)
33
+ self.bn2 = nn.BatchNorm1d(filterno_2)
34
+ self.relu2 = nn.ReLU()
35
+
36
+ def forward(self, x):
37
+ x = self.conv1(x)
38
+ x = self.bn1(x)
39
+ x = self.relu1(x)
40
+ x = self.conv2(x)
41
+ x = self.bn2(x)
42
+ x = self.relu2(x)
43
+ return x
44
+
45
+
46
+ class KanResModule(nn.Module):
47
+ """Residual module with skip connection for KanResWideX architecture.
48
+
49
+ Args:
50
+ in_channels: Number of input channels
51
+ filterno_1: Number of filters in first convolution
52
+ filterno_2: Number of filters in second convolution
53
+ filtersize_1: Kernel size for first convolution
54
+ filtersize_2: Kernel size for second convolution
55
+ stride: Stride for first convolution
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ in_channels: int,
61
+ filterno_1: int,
62
+ filterno_2: int,
63
+ filtersize_1: int,
64
+ filtersize_2: int,
65
+ stride: int,
66
+ ):
67
+ super().__init__()
68
+ self.conv1 = nn.Conv1d(in_channels, filterno_1, filtersize_1, stride=stride, padding="same")
69
+ self.bn1 = nn.BatchNorm1d(filterno_1)
70
+ self.relu1 = nn.ReLU()
71
+ self.conv2 = nn.Conv1d(filterno_1, filterno_2, filtersize_2, padding="same")
72
+ self.bn2 = nn.BatchNorm1d(filterno_2)
73
+ self.relu2 = nn.ReLU()
74
+
75
+ def forward(self, x):
76
+ identity = x
77
+ out = self.conv1(x)
78
+ out = self.bn1(out)
79
+ out = self.relu1(out)
80
+ out = self.conv2(out)
81
+ out = self.bn2(out)
82
+ out = self.relu2(out)
83
+ out = out + identity
84
+ return out
85
+
86
+
87
+ @register_model(
88
+ name="kanres-deep",
89
+ description="Deep KAN-ResNet architecture",
90
+ )
91
+ class KanResDeepX(nn.Module):
92
+ """KanRes-Deep-X model for ECG signal classification.
93
+
94
+ A deep residual convolutional neural network architecture designed for ECG
95
+ signal analysis with 8 residual blocks for improved feature extraction.
96
+
97
+ Args:
98
+ input_channels: Number of input channels (default: 1 for single-lead ECG)
99
+ output_size: Number of output classes (default: 4)
100
+ base_channels: Base number of channels for the architecture (default: 32)
101
+
102
+ Example:
103
+ >>> model = KanResDeepX(input_channels=1, output_size=4, base_channels=32)
104
+ >>> x = torch.randn(32, 1, 3000)
105
+ >>> output = model(x)
106
+ >>> print(output.shape)
107
+
108
+ >>> features = model.extract_features(x)
109
+ >>> print(features.shape) # (32, 32)
110
+ """
111
+
112
+ def __init__(self, input_channels: int = 1, output_size: int = 4, base_channels: int = 32):
113
+ super().__init__()
114
+
115
+ self.base_channels = base_channels
116
+ init_channels = base_channels * 2
117
+
118
+ self.init_block = KanResInit(input_channels, init_channels, base_channels, 8, 3, 1)
119
+ self.pool = nn.AvgPool1d(kernel_size=2)
120
+
121
+ self.res_modules = nn.ModuleList(
122
+ [
123
+ KanResModule(base_channels, base_channels * 2, base_channels, 50, 50, 1)
124
+ for _ in range(8)
125
+ ]
126
+ )
127
+
128
+ self.global_pool = nn.AdaptiveAvgPool1d(1)
129
+ self._feature_dim = base_channels
130
+ self.fc = nn.Linear(base_channels, output_size)
131
+
132
+ @property
133
+ def feature_dim(self) -> int:
134
+ return self._feature_dim
135
+
136
+ def extract_features(self, x: torch.Tensor) -> torch.Tensor:
137
+ x = self.init_block(x)
138
+ x = self.pool(x)
139
+
140
+ for res_module in self.res_modules:
141
+ x = res_module(x)
142
+
143
+ x = self.global_pool(x)
144
+ x = x.squeeze(-1)
145
+ return x
146
+
147
+ def forward(self, x):
148
+ x = self.extract_features(x)
149
+ x = self.fc(x)
150
+ return x
@@ -0,0 +1,126 @@
1
+ from pathlib import Path
2
+ from typing import Optional, Union
3
+
4
+ import torch
5
+ import torch.nn.functional as func
6
+ from torch import nn
7
+
8
+ from deepecgkit.registry import register_model
9
+ from deepecgkit.utils.weights import get_weight_info, load_pretrained_weights
10
+
11
+
12
+ class ConvBlock(nn.Module):
13
+ """Convolutional block with normalization and activation."""
14
+
15
+ def __init__(self, in_channels: int, out_channels: int, kernel_size: int = 3):
16
+ super().__init__()
17
+ self.conv = nn.Conv1d(in_channels, out_channels, kernel_size, padding=kernel_size // 2)
18
+ self.bn = nn.BatchNorm1d(out_channels)
19
+
20
+ def forward(self, x):
21
+ return func.relu(self.bn(self.conv(x)))
22
+
23
+
24
+ class KanResModule(nn.Module):
25
+ """Residual module for the KanResWideX architecture."""
26
+
27
+ def __init__(self, channels: int):
28
+ super().__init__()
29
+ self.conv1 = ConvBlock(channels, channels)
30
+ self.conv2 = ConvBlock(channels, channels)
31
+
32
+ def forward(self, x):
33
+ residual = x
34
+ out = self.conv1(x)
35
+ out = self.conv2(out)
36
+ return out + residual
37
+
38
+
39
+ @register_model(
40
+ name="kanres",
41
+ description="KAN-ResNet architecture with wide layers",
42
+ )
43
+ class KanResWideX(nn.Module):
44
+ """
45
+ KanRes-Wide-X model for ECG signal classification.
46
+
47
+ A convolutional neural network architecture designed for ECG signal analysis
48
+ with residual connections and wide blocks for improved feature extraction.
49
+
50
+ Args:
51
+ input_channels: Number of input channels (default: 1 for single-lead ECG)
52
+ output_size: Number of output classes or regression targets
53
+ base_channels: Base number of channels for the first layer (default: 64)
54
+
55
+ Example:
56
+ >>> model = KanResWideX(input_channels=1, output_size=4)
57
+ >>> x = torch.randn(32, 1, 3000)
58
+ >>> output = model(x)
59
+ >>> print(output.shape) # [32, 4]
60
+
61
+ >>> features = model.extract_features(x)
62
+ >>> print(features.shape) # (32, 64)
63
+ """
64
+
65
+ def __init__(self, input_channels: int = 1, output_size: int = 4, base_channels: int = 64):
66
+ super().__init__()
67
+
68
+ self.input_layer = ConvBlock(input_channels, base_channels)
69
+ self.res_modules = nn.Sequential(
70
+ KanResModule(base_channels), KanResModule(base_channels), KanResModule(base_channels)
71
+ )
72
+ self.global_pool = nn.AdaptiveAvgPool1d(1)
73
+ self._feature_dim = base_channels
74
+ self.classifier = nn.Linear(base_channels, output_size)
75
+
76
+ @property
77
+ def feature_dim(self) -> int:
78
+ return self._feature_dim
79
+
80
+ def extract_features(self, x: torch.Tensor) -> torch.Tensor:
81
+ x = self.input_layer(x)
82
+ x = self.res_modules(x)
83
+ x = self.global_pool(x)
84
+ x = x.squeeze(-1)
85
+ return x
86
+
87
+ def forward(self, x):
88
+ x = self.extract_features(x)
89
+ return self.classifier(x)
90
+
91
+ @classmethod
92
+ def from_pretrained(
93
+ cls,
94
+ weights: str,
95
+ map_location: Optional[Union[str, torch.device]] = None,
96
+ force_download: bool = False,
97
+ **kwargs,
98
+ ) -> "KanResWideX":
99
+ """Load a pretrained KanResWideX model.
100
+
101
+ Args:
102
+ weights: Name of pretrained weights (e.g., "kanres-af-30s") or path to weights file
103
+ map_location: Device to map weights to (e.g., "cpu", "cuda")
104
+ force_download: If True, re-download weights even if cached
105
+ **kwargs: Override default model parameters from the weight registry
106
+
107
+ Returns:
108
+ Model with pretrained weights loaded
109
+
110
+ Example:
111
+ >>> model = KanResWideX.from_pretrained("kanres-af-30s")
112
+ >>> model = KanResWideX.from_pretrained("kanres-af-30s", map_location="cuda")
113
+ >>> model = KanResWideX.from_pretrained("/path/to/weights.pt", output_size=2)
114
+ """
115
+ weight_path = Path(weights)
116
+ if weight_path.exists():
117
+ state_dict = torch.load(weight_path, map_location=map_location, weights_only=True)
118
+ model = cls(**kwargs)
119
+ else:
120
+ info = get_weight_info(weights)
121
+ model_kwargs = {**info["model_kwargs"], **kwargs}
122
+ model = cls(**model_kwargs)
123
+ state_dict = load_pretrained_weights(weights, map_location, force_download)
124
+
125
+ model.load_state_dict(state_dict)
126
+ return model
@@ -0,0 +1,96 @@
1
+ import torch
2
+ from torch import nn
3
+
4
+ from deepecgkit.registry import register_model
5
+
6
+
7
+ @register_model(
8
+ name="lstm",
9
+ description="LSTM-based model for sequential ECG analysis",
10
+ )
11
+ class LSTMECG(nn.Module):
12
+ """
13
+ LSTM-based model for ECG signal classification.
14
+
15
+ A recurrent neural network using bidirectional LSTM layers
16
+ for temporal pattern recognition in ECG signals.
17
+
18
+ Args:
19
+ input_channels: Number of input channels (default: 1 for single-lead ECG)
20
+ output_size: Number of output classes
21
+ hidden_size: Size of LSTM hidden state (default: 128)
22
+ num_layers: Number of LSTM layers (default: 2)
23
+ dropout_rate: Dropout probability (default: 0.3)
24
+ bidirectional: Use bidirectional LSTM (default: True)
25
+
26
+ Example:
27
+ >>> model = LSTMECG(input_channels=1, output_size=4)
28
+ >>> x = torch.randn(32, 1, 3000)
29
+ >>> output = model(x)
30
+ >>> print(output.shape)
31
+
32
+ >>> features = model.extract_features(x)
33
+ >>> print(features.shape) # (32, 256) with bidirectional=True
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ input_channels: int = 1,
39
+ output_size: int = 4,
40
+ hidden_size: int = 128,
41
+ num_layers: int = 2,
42
+ dropout_rate: float = 0.3,
43
+ bidirectional: bool = True,
44
+ ):
45
+ super().__init__()
46
+
47
+ self.input_channels = input_channels
48
+ self.hidden_size = hidden_size
49
+ self.num_layers = num_layers
50
+ self.bidirectional = bidirectional
51
+ self.num_directions = 2 if bidirectional else 1
52
+
53
+ self.lstm = nn.LSTM(
54
+ input_size=input_channels,
55
+ hidden_size=hidden_size,
56
+ num_layers=num_layers,
57
+ batch_first=True,
58
+ dropout=dropout_rate if num_layers > 1 else 0,
59
+ bidirectional=bidirectional,
60
+ )
61
+
62
+ self.dropout = nn.Dropout(dropout_rate)
63
+ self._feature_dim = hidden_size * self.num_directions
64
+
65
+ self.classifier = nn.Sequential(
66
+ nn.Linear(self._feature_dim, 64),
67
+ nn.ReLU(),
68
+ nn.Dropout(dropout_rate),
69
+ nn.Linear(64, output_size),
70
+ )
71
+
72
+ @property
73
+ def feature_dim(self) -> int:
74
+ return self._feature_dim
75
+
76
+ def _extract_lstm_features(self, x: torch.Tensor) -> torch.Tensor:
77
+ batch_size = x.size(0)
78
+ x = x.transpose(1, 2)
79
+ _, (h_n, _) = self.lstm(x)
80
+
81
+ if self.bidirectional:
82
+ h_n = h_n.view(self.num_layers, 2, batch_size, self.hidden_size)
83
+ h_n = h_n[-1]
84
+ h_n = torch.cat([h_n[0], h_n[1]], dim=1)
85
+ else:
86
+ h_n = h_n[-1]
87
+
88
+ return h_n
89
+
90
+ def extract_features(self, x: torch.Tensor) -> torch.Tensor:
91
+ return self._extract_lstm_features(x)
92
+
93
+ def forward(self, x):
94
+ x = self.extract_features(x)
95
+ x = self.dropout(x)
96
+ return self.classifier(x)