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,117 @@
1
+ import torch
2
+ from torch import nn
3
+
4
+ from deepecgkit.registry import register_model
5
+
6
+
7
+ @register_model(
8
+ name="crnn",
9
+ description="CNN-LSTM hybrid for local feature extraction and temporal aggregation",
10
+ )
11
+ class CRNN(nn.Module):
12
+ """
13
+ Convolutional Recurrent Neural Network for ECG classification.
14
+
15
+ Uses a CNN front-end for local feature extraction followed by a
16
+ bidirectional LSTM for temporal aggregation, combining the strengths
17
+ of both architectures.
18
+
19
+ Args:
20
+ input_channels: Number of input channels (default: 1 for single-lead ECG)
21
+ output_size: Number of output classes
22
+ cnn_channels: List of channel sizes for CNN stages (default: [32, 64, 128, 256])
23
+ lstm_hidden_size: Size of LSTM hidden state (default: 128)
24
+ lstm_num_layers: Number of LSTM layers (default: 2)
25
+ bidirectional: Use bidirectional LSTM (default: True)
26
+ dropout_rate: Dropout probability (default: 0.3)
27
+
28
+ Example:
29
+ >>> model = CRNN(input_channels=1, output_size=4)
30
+ >>> x = torch.randn(32, 1, 3000)
31
+ >>> output = model(x)
32
+ >>> print(output.shape)
33
+
34
+ >>> features = model.extract_features(x)
35
+ >>> print(features.shape) # (32, 256)
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ input_channels: int = 1,
41
+ output_size: int = 4,
42
+ cnn_channels: list | None = None,
43
+ lstm_hidden_size: int = 128,
44
+ lstm_num_layers: int = 2,
45
+ bidirectional: bool = True,
46
+ dropout_rate: float = 0.3,
47
+ ):
48
+ super().__init__()
49
+
50
+ if cnn_channels is None:
51
+ cnn_channels = [32, 64, 128, 256]
52
+
53
+ self.lstm_hidden_size = lstm_hidden_size
54
+ self.lstm_num_layers = lstm_num_layers
55
+ self.bidirectional = bidirectional
56
+ self.num_directions = 2 if bidirectional else 1
57
+
58
+ layers = []
59
+ in_ch = input_channels
60
+ kernel_sizes = [7, 5, 3, 3]
61
+ for i, out_ch in enumerate(cnn_channels):
62
+ ks = kernel_sizes[i] if i < len(kernel_sizes) else 3
63
+ layers.extend(
64
+ [
65
+ nn.Conv1d(in_ch, out_ch, kernel_size=ks, padding=ks // 2),
66
+ nn.BatchNorm1d(out_ch),
67
+ nn.ReLU(),
68
+ nn.MaxPool1d(kernel_size=2),
69
+ nn.Dropout(dropout_rate),
70
+ ]
71
+ )
72
+ in_ch = out_ch
73
+
74
+ self.cnn = nn.Sequential(*layers)
75
+
76
+ self.lstm = nn.LSTM(
77
+ input_size=cnn_channels[-1],
78
+ hidden_size=lstm_hidden_size,
79
+ num_layers=lstm_num_layers,
80
+ batch_first=True,
81
+ dropout=dropout_rate if lstm_num_layers > 1 else 0,
82
+ bidirectional=bidirectional,
83
+ )
84
+
85
+ self.dropout = nn.Dropout(dropout_rate)
86
+ self._feature_dim = lstm_hidden_size * self.num_directions
87
+
88
+ self.classifier = nn.Sequential(
89
+ nn.Linear(self._feature_dim, 64),
90
+ nn.ReLU(),
91
+ nn.Dropout(dropout_rate),
92
+ nn.Linear(64, output_size),
93
+ )
94
+
95
+ @property
96
+ def feature_dim(self) -> int:
97
+ return self._feature_dim
98
+
99
+ def extract_features(self, x: torch.Tensor) -> torch.Tensor:
100
+ batch_size = x.size(0)
101
+ x = self.cnn(x)
102
+ x = x.transpose(1, 2)
103
+ _, (h_n, _) = self.lstm(x)
104
+
105
+ if self.bidirectional:
106
+ h_n = h_n.view(self.lstm_num_layers, 2, batch_size, self.lstm_hidden_size)
107
+ h_n = h_n[-1]
108
+ h_n = torch.cat([h_n[0], h_n[1]], dim=1)
109
+ else:
110
+ h_n = h_n[-1]
111
+
112
+ return h_n
113
+
114
+ def forward(self, x):
115
+ x = self.extract_features(x)
116
+ x = self.dropout(x)
117
+ return self.classifier(x)
@@ -0,0 +1,215 @@
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 StemResBlock2D(nn.Module):
9
+ """
10
+ First residual block following the stem convolution.
11
+
12
+ Unlike subsequent blocks, this starts directly with convolution
13
+ (no pre-activation BN/ReLU), matching the Keras reference architecture.
14
+ All convolutions use valid padding (no padding).
15
+
16
+ Args:
17
+ in_channels: Number of input filter channels
18
+ out_channels: Number of output filter channels
19
+ dropout_rate: Dropout probability
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ in_channels: int,
25
+ out_channels: int,
26
+ dropout_rate: float = 0.1,
27
+ ):
28
+ super().__init__()
29
+
30
+ self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=(1, 5))
31
+ self.bn1 = nn.BatchNorm2d(out_channels)
32
+ self.dropout1 = nn.Dropout(dropout_rate)
33
+ self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=(1, 5), stride=(1, 2))
34
+
35
+ self.shortcut_pool = nn.MaxPool2d(kernel_size=(1, 9), stride=(1, 2))
36
+ self.shortcut_proj = nn.Conv2d(in_channels, out_channels, kernel_size=1)
37
+
38
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
39
+ out = self.conv1(x)
40
+ out = self.bn1(out)
41
+ out = func.relu(out)
42
+ out = self.dropout1(out)
43
+ out = self.conv2(out)
44
+
45
+ shortcut = self.shortcut_pool(x)
46
+ shortcut = self.shortcut_proj(shortcut)
47
+
48
+ return out + shortcut
49
+
50
+
51
+ class PreActResBlock2D(nn.Module):
52
+ """
53
+ Pre-activation 2D residual block for blocks 2-4.
54
+
55
+ Follows BN -> ReLU -> Dropout -> Conv -> BN -> ReLU -> Dropout -> Conv(stride=2)
56
+ pattern matching the Keras reference model. All convolutions use valid padding.
57
+
58
+ Args:
59
+ in_channels: Number of input filter channels
60
+ out_channels: Number of output filter channels
61
+ dropout_rate: Dropout probability
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ in_channels: int,
67
+ out_channels: int,
68
+ dropout_rate: float = 0.1,
69
+ ):
70
+ super().__init__()
71
+
72
+ self.bn1 = nn.BatchNorm2d(in_channels)
73
+ self.dropout1 = nn.Dropout(dropout_rate)
74
+ self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=(1, 5))
75
+ self.bn2 = nn.BatchNorm2d(out_channels)
76
+ self.dropout2 = nn.Dropout(dropout_rate)
77
+ self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=(1, 5), stride=(1, 2))
78
+
79
+ self.shortcut_pool = nn.MaxPool2d(kernel_size=(1, 9), stride=(1, 2))
80
+ self.shortcut_proj = (
81
+ nn.Conv2d(in_channels, out_channels, kernel_size=1)
82
+ if in_channels != out_channels
83
+ else nn.Identity()
84
+ )
85
+
86
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
87
+ out = self.bn1(x)
88
+ out = func.relu(out)
89
+ out = self.dropout1(out)
90
+ out = self.conv1(out)
91
+
92
+ out = self.bn2(out)
93
+ out = func.relu(out)
94
+ out = self.dropout2(out)
95
+ out = self.conv2(out)
96
+
97
+ shortcut = self.shortcut_pool(x)
98
+ shortcut = self.shortcut_proj(shortcut)
99
+
100
+ return out + shortcut
101
+
102
+
103
+ @register_model(
104
+ name="deep-res-cnn",
105
+ description="Deep Residual 2D CNN (Elyamani et al. 2022) for multi-lead ECG classification",
106
+ )
107
+ class DeepResCNN(nn.Module):
108
+ """
109
+ Deep Residual 2D CNN for ECG classification.
110
+
111
+ Faithful implementation of Elyamani et al. (2022). Uses Conv2d with (1, k)
112
+ kernels and valid padding to process each ECG lead independently along the
113
+ time axis, then fuses across leads with a (leads, 1) convolution at the end.
114
+
115
+ Input convention: (batch, leads, time) -- standard deepecg-kit format.
116
+ Internally reshaped to (batch, 1, leads, time) for 2D convolution.
117
+
118
+ The classifier head includes L2 regularization matching the original Keras
119
+ model. Call l2_regularization_loss() to obtain the penalty term.
120
+
121
+ Reference:
122
+ https://github.com/HaneenElyamani/ECG-classification
123
+
124
+ Args:
125
+ input_channels: Number of ECG leads (default: 12)
126
+ output_size: Number of output classes (default: 5)
127
+ dropout_rate: Dropout probability in residual blocks (default: 0.1)
128
+
129
+ Example:
130
+ >>> model = DeepResCNN(input_channels=12, output_size=5)
131
+ >>> x = torch.randn(32, 12, 1000)
132
+ >>> output = model(x)
133
+ >>> print(output.shape)
134
+
135
+ >>> features = model.extract_features(x)
136
+ >>> print(features.shape) # (32, 128)
137
+ """
138
+
139
+ def __init__(
140
+ self,
141
+ input_channels: int = 12,
142
+ output_size: int = 5,
143
+ dropout_rate: float = 0.1,
144
+ ):
145
+ super().__init__()
146
+
147
+ self.stem_conv = nn.Conv2d(1, 32, kernel_size=(1, 7))
148
+ self.stem_bn = nn.BatchNorm2d(32)
149
+
150
+ self.block1 = StemResBlock2D(32, 64, dropout_rate=dropout_rate)
151
+ self.block2 = PreActResBlock2D(64, 64, dropout_rate=dropout_rate)
152
+ self.block3 = PreActResBlock2D(64, 128, dropout_rate=dropout_rate)
153
+ self.block4 = PreActResBlock2D(128, 128, dropout_rate=dropout_rate)
154
+
155
+ self.lead_fusion = nn.Conv2d(128, 128, kernel_size=(input_channels, 1))
156
+ self.lead_fusion_bn = nn.BatchNorm2d(128)
157
+ self.pool = nn.AdaptiveAvgPool2d(1)
158
+
159
+ self._feature_dim = 128
160
+
161
+ self.fc1 = nn.Linear(128, 128)
162
+ self.fc1_bn = nn.BatchNorm1d(128)
163
+ self.fc1_drop = nn.Dropout(dropout_rate)
164
+
165
+ self.fc2 = nn.Linear(128, 64)
166
+ self.fc2_bn = nn.BatchNorm1d(64)
167
+ self.fc2_drop = nn.Dropout(0.15)
168
+
169
+ self.fc_out = nn.Linear(64, output_size)
170
+
171
+ @property
172
+ def feature_dim(self) -> int:
173
+ return self._feature_dim
174
+
175
+ def l2_regularization_loss(self) -> torch.Tensor:
176
+ """Return the L2 penalty for the classifier head weights.
177
+
178
+ Matches the Keras kernel_regularizer=L2(lambda) on the two Dense layers.
179
+ Add this to the training loss for full equivalence with the original model.
180
+ """
181
+ return 0.005 * self.fc1.weight.pow(2).sum() + 0.009 * self.fc2.weight.pow(2).sum()
182
+
183
+ def extract_features(self, x: torch.Tensor) -> torch.Tensor:
184
+ x = x.unsqueeze(1)
185
+
186
+ x = self.stem_conv(x)
187
+ x = self.stem_bn(x)
188
+ x = func.relu(x)
189
+
190
+ x = self.block1(x)
191
+ x = self.block2(x)
192
+ x = self.block3(x)
193
+ x = self.block4(x)
194
+
195
+ x = self.lead_fusion(x)
196
+ x = self.lead_fusion_bn(x)
197
+ x = func.relu(x)
198
+ x = self.pool(x)
199
+ return torch.flatten(x, 1)
200
+
201
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
202
+ x = self.extract_features(x)
203
+
204
+ x = self.fc1(x)
205
+ x = self.fc1_bn(x)
206
+ x = func.relu(x)
207
+ x = self.fc1_drop(x)
208
+
209
+ x = self.fc2(x)
210
+ x = self.fc2_bn(x)
211
+ x = func.relu(x)
212
+ x = self.fc2_drop(x)
213
+
214
+ x = self.fc_out(x)
215
+ return x
@@ -0,0 +1,230 @@
1
+ import math
2
+
3
+ import torch
4
+ from torch import nn
5
+
6
+ from deepecgkit.registry import register_model
7
+
8
+
9
+ class PositionalEncoding(nn.Module):
10
+ """
11
+ Sinusoidal positional encoding for transformer branch.
12
+
13
+ Args:
14
+ d_model: Dimension of the model
15
+ max_len: Maximum sequence length (default: 5000)
16
+ dropout: Dropout probability (default: 0.1)
17
+ """
18
+
19
+ def __init__(self, d_model: int, max_len: int = 5000, dropout: float = 0.1):
20
+ super().__init__()
21
+ self.dropout = nn.Dropout(p=dropout)
22
+
23
+ position = torch.arange(max_len).unsqueeze(1)
24
+ div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))
25
+ pe = torch.zeros(1, max_len, d_model)
26
+ pe[0, :, 0::2] = torch.sin(position * div_term)
27
+ pe[0, :, 1::2] = torch.cos(position * div_term)
28
+ self.register_buffer("pe", pe)
29
+
30
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
31
+ x = x + self.pe[:, : x.size(1)]
32
+ return self.dropout(x)
33
+
34
+
35
+ class CNNLSTMBranch(nn.Module):
36
+ """
37
+ CNN feature extraction followed by LSTM temporal aggregation.
38
+
39
+ Args:
40
+ input_channels: Number of input ECG channels
41
+ cnn_channels: Output channels for CNN stages (default: 128)
42
+ lstm_hidden: LSTM hidden size (default: 128)
43
+ lstm_layers: Number of LSTM layers (default: 1)
44
+ dropout_rate: Dropout probability (default: 0.3)
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ input_channels: int,
50
+ cnn_channels: int = 128,
51
+ lstm_hidden: int = 128,
52
+ lstm_layers: int = 1,
53
+ dropout_rate: float = 0.3,
54
+ ):
55
+ super().__init__()
56
+ self.lstm_hidden = lstm_hidden
57
+
58
+ self.cnn = nn.Sequential(
59
+ nn.Conv1d(input_channels, 64, kernel_size=7, padding=3),
60
+ nn.BatchNorm1d(64),
61
+ nn.ReLU(),
62
+ nn.MaxPool1d(kernel_size=2),
63
+ nn.Dropout(dropout_rate),
64
+ nn.Conv1d(64, cnn_channels, kernel_size=5, padding=2),
65
+ nn.BatchNorm1d(cnn_channels),
66
+ nn.ReLU(),
67
+ nn.MaxPool1d(kernel_size=2),
68
+ nn.Dropout(dropout_rate),
69
+ nn.Conv1d(cnn_channels, cnn_channels, kernel_size=3, padding=1),
70
+ nn.BatchNorm1d(cnn_channels),
71
+ nn.ReLU(),
72
+ nn.MaxPool1d(kernel_size=2),
73
+ )
74
+
75
+ self.lstm = nn.LSTM(
76
+ input_size=cnn_channels,
77
+ hidden_size=lstm_hidden,
78
+ num_layers=lstm_layers,
79
+ batch_first=True,
80
+ dropout=dropout_rate if lstm_layers > 1 else 0,
81
+ bidirectional=True,
82
+ )
83
+
84
+ self.output_dim = lstm_hidden * 2
85
+
86
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
87
+ batch_size = x.size(0)
88
+ x = self.cnn(x)
89
+ x = x.transpose(1, 2)
90
+ _, (h_n, _) = self.lstm(x)
91
+ h_n = h_n.view(-1, 2, batch_size, self.lstm_hidden)
92
+ h_n = h_n[-1]
93
+ return torch.cat([h_n[0], h_n[1]], dim=1)
94
+
95
+
96
+ class TransformerBranch(nn.Module):
97
+ """
98
+ Transformer encoder for global temporal attention.
99
+
100
+ Args:
101
+ input_channels: Number of input ECG channels
102
+ d_model: Transformer model dimension (default: 128)
103
+ nhead: Number of attention heads (default: 8)
104
+ num_layers: Number of transformer encoder layers (default: 2)
105
+ dim_feedforward: Feedforward dimension (default: 256)
106
+ dropout_rate: Dropout probability (default: 0.1)
107
+ """
108
+
109
+ def __init__(
110
+ self,
111
+ input_channels: int,
112
+ d_model: int = 128,
113
+ nhead: int = 8,
114
+ num_layers: int = 2,
115
+ dim_feedforward: int = 256,
116
+ dropout_rate: float = 0.1,
117
+ ):
118
+ super().__init__()
119
+
120
+ self.input_projection = nn.Linear(input_channels, d_model)
121
+ self.positional_encoding = PositionalEncoding(d_model, dropout=dropout_rate)
122
+
123
+ encoder_layer = nn.TransformerEncoderLayer(
124
+ d_model=d_model,
125
+ nhead=nhead,
126
+ dim_feedforward=dim_feedforward,
127
+ dropout=dropout_rate,
128
+ batch_first=True,
129
+ )
130
+
131
+ self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
132
+ self.pool = nn.AdaptiveAvgPool1d(1)
133
+ self.output_dim = d_model
134
+
135
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
136
+ x = x.transpose(1, 2)
137
+ x = self.input_projection(x)
138
+ x = self.positional_encoding(x)
139
+ x = self.encoder(x)
140
+ x = x.transpose(1, 2)
141
+ x = self.pool(x)
142
+ return x.squeeze(-1)
143
+
144
+
145
+ @register_model(
146
+ name="dualnet",
147
+ description="Dual-path architecture with CNN-LSTM and Transformer branches for ECG",
148
+ )
149
+ class ECGDualNet(nn.Module):
150
+ """
151
+ Dual-path ECG classification network.
152
+
153
+ Runs a CNN-LSTM branch and a Transformer branch in parallel,
154
+ then fuses their outputs via concatenation and a fusion head.
155
+
156
+ Args:
157
+ input_channels: Number of input channels (default: 1 for single-lead ECG)
158
+ output_size: Number of output classes
159
+ cnn_channels: CNN output channels in CNN-LSTM branch (default: 128)
160
+ lstm_hidden: LSTM hidden size in CNN-LSTM branch (default: 128)
161
+ lstm_layers: Number of LSTM layers (default: 1)
162
+ d_model: Transformer model dimension (default: 128)
163
+ nhead: Number of attention heads (default: 8)
164
+ transformer_layers: Number of transformer encoder layers (default: 2)
165
+ dim_feedforward: Transformer feedforward dimension (default: 256)
166
+ dropout_rate: Dropout probability (default: 0.3)
167
+
168
+ Example:
169
+ >>> model = ECGDualNet(input_channels=1, output_size=4)
170
+ >>> x = torch.randn(32, 1, 3000)
171
+ >>> output = model(x)
172
+ >>> print(output.shape)
173
+
174
+ >>> features = model.extract_features(x)
175
+ >>> print(features.shape) # (32, 384)
176
+ """
177
+
178
+ def __init__(
179
+ self,
180
+ input_channels: int = 1,
181
+ output_size: int = 4,
182
+ cnn_channels: int = 128,
183
+ lstm_hidden: int = 128,
184
+ lstm_layers: int = 1,
185
+ d_model: int = 128,
186
+ nhead: int = 8,
187
+ transformer_layers: int = 2,
188
+ dim_feedforward: int = 256,
189
+ dropout_rate: float = 0.3,
190
+ ):
191
+ super().__init__()
192
+
193
+ self.cnn_lstm_branch = CNNLSTMBranch(
194
+ input_channels=input_channels,
195
+ cnn_channels=cnn_channels,
196
+ lstm_hidden=lstm_hidden,
197
+ lstm_layers=lstm_layers,
198
+ dropout_rate=dropout_rate,
199
+ )
200
+
201
+ self.transformer_branch = TransformerBranch(
202
+ input_channels=input_channels,
203
+ d_model=d_model,
204
+ nhead=nhead,
205
+ num_layers=transformer_layers,
206
+ dim_feedforward=dim_feedforward,
207
+ dropout_rate=dropout_rate,
208
+ )
209
+
210
+ self._feature_dim = self.cnn_lstm_branch.output_dim + self.transformer_branch.output_dim
211
+
212
+ self.classifier = nn.Sequential(
213
+ nn.Linear(self._feature_dim, 128),
214
+ nn.ReLU(),
215
+ nn.Dropout(dropout_rate),
216
+ nn.Linear(128, output_size),
217
+ )
218
+
219
+ @property
220
+ def feature_dim(self) -> int:
221
+ return self._feature_dim
222
+
223
+ def extract_features(self, x: torch.Tensor) -> torch.Tensor:
224
+ cnn_lstm_out = self.cnn_lstm_branch(x)
225
+ transformer_out = self.transformer_branch(x)
226
+ return torch.cat([cnn_lstm_out, transformer_out], dim=1)
227
+
228
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
229
+ x = self.extract_features(x)
230
+ return self.classifier(x)
@@ -0,0 +1,88 @@
1
+ import torch
2
+ from torch import nn
3
+
4
+ from deepecgkit.registry import register_model
5
+
6
+
7
+ @register_model(
8
+ name="fcn-wang",
9
+ description="Fully Convolutional Network (Wang et al.) for time series classification",
10
+ )
11
+ class FCNWang(nn.Module):
12
+ """
13
+ Fully Convolutional Network for ECG signal classification.
14
+
15
+ Based on Wang et al.'s FCN architecture for time series classification,
16
+ using three convolutional blocks with batch normalization and global
17
+ average pooling. No dense layers except the final classifier.
18
+
19
+ Reference:
20
+ Wang Z., Yan W., Oates T. "Time Series Classification from Scratch
21
+ with Deep Neural Networks: A Strong Baseline" (2017)
22
+ https://github.com/helme/ecg_ptbxl_benchmarking
23
+
24
+ Args:
25
+ input_channels: Number of input channels (default: 12 for 12-lead ECG)
26
+ output_size: Number of output classes
27
+ filters: List of filter counts for each conv block (default: [128, 256, 128])
28
+ kernel_sizes: List of kernel sizes for each conv block (default: [8, 5, 3])
29
+ dropout_rate: Dropout probability before classifier (default: 0.3)
30
+
31
+ Example:
32
+ >>> model = FCNWang(input_channels=12, output_size=5)
33
+ >>> x = torch.randn(32, 12, 1000)
34
+ >>> output = model(x)
35
+ >>> print(output.shape)
36
+
37
+ >>> features = model.extract_features(x)
38
+ >>> print(features.shape) # (32, 128)
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ input_channels: int = 12,
44
+ output_size: int = 5,
45
+ filters: list[int] | None = None,
46
+ kernel_sizes: list[int] | None = None,
47
+ dropout_rate: float = 0.3,
48
+ ):
49
+ super().__init__()
50
+
51
+ if filters is None:
52
+ filters = [128, 256, 128]
53
+ if kernel_sizes is None:
54
+ kernel_sizes = [8, 5, 3]
55
+
56
+ assert len(filters) == len(kernel_sizes)
57
+
58
+ blocks = []
59
+ in_ch = input_channels
60
+ for nf, ks in zip(filters, kernel_sizes):
61
+ blocks.append(
62
+ nn.Sequential(
63
+ nn.Conv1d(in_ch, nf, kernel_size=ks, padding=(ks - 1) // 2, bias=False),
64
+ nn.BatchNorm1d(nf),
65
+ nn.ReLU(inplace=True),
66
+ )
67
+ )
68
+ in_ch = nf
69
+
70
+ self.conv_blocks = nn.Sequential(*blocks)
71
+ self.global_pool = nn.AdaptiveAvgPool1d(1)
72
+ self.dropout = nn.Dropout(dropout_rate)
73
+ self._feature_dim = filters[-1]
74
+ self.classifier = nn.Linear(self._feature_dim, 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.conv_blocks(x)
82
+ x = self.global_pool(x)
83
+ return torch.flatten(x, 1)
84
+
85
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
86
+ x = self.extract_features(x)
87
+ x = self.dropout(x)
88
+ return self.classifier(x)