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,182 @@
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 BasicBlock1DWang(nn.Module):
9
+ """
10
+ Basic residual block for the Wang ResNet variant.
11
+
12
+ Uses two convolutional layers with batch normalization and a skip connection.
13
+ The second convolution uses a smaller kernel (kernel_size // 2 + 1).
14
+
15
+ Args:
16
+ in_channels: Number of input channels
17
+ out_channels: Number of output channels
18
+ kernel_size: Primary kernel size (default: 5)
19
+ stride: Stride for the first convolution (default: 1)
20
+ downsample: Optional downsample module for the skip connection
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ in_channels: int,
26
+ out_channels: int,
27
+ kernel_size: int = 5,
28
+ stride: int = 1,
29
+ downsample: nn.Module | None = None,
30
+ ):
31
+ super().__init__()
32
+
33
+ ks2 = kernel_size // 2 + 1
34
+
35
+ self.conv1 = nn.Conv1d(
36
+ in_channels,
37
+ out_channels,
38
+ kernel_size=kernel_size,
39
+ stride=stride,
40
+ padding=(kernel_size - 1) // 2,
41
+ bias=False,
42
+ )
43
+ self.bn1 = nn.BatchNorm1d(out_channels)
44
+ self.conv2 = nn.Conv1d(
45
+ out_channels,
46
+ out_channels,
47
+ kernel_size=ks2,
48
+ stride=1,
49
+ padding=(ks2 - 1) // 2,
50
+ bias=False,
51
+ )
52
+ self.bn2 = nn.BatchNorm1d(out_channels)
53
+ self.downsample = downsample
54
+
55
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
56
+ identity = x
57
+
58
+ out = func.relu(self.bn1(self.conv1(x)), inplace=True)
59
+ out = self.bn2(self.conv2(out))
60
+
61
+ if self.downsample is not None:
62
+ identity = self.downsample(x)
63
+
64
+ out += identity
65
+ return func.relu(out, inplace=True)
66
+
67
+
68
+ @register_model(
69
+ name="resnet-wang",
70
+ description="ResNet (Wang et al.) for time series classification",
71
+ )
72
+ class ResNetWang(nn.Module):
73
+ """
74
+ ResNet model based on Wang et al.'s architecture for time series classification.
75
+
76
+ A shallow 3-block residual network without initial pooling, using larger
77
+ initial channels (128) and asymmetric kernel sizes in residual blocks.
78
+ This is the standard ResNet baseline from the PTB-XL benchmark.
79
+
80
+ Reference:
81
+ Wang Z., Yan W., Oates T. "Time Series Classification from Scratch
82
+ with Deep Neural Networks: A Strong Baseline" (2017)
83
+ https://github.com/helme/ecg_ptbxl_benchmarking
84
+
85
+ Args:
86
+ input_channels: Number of input channels (default: 12 for 12-lead ECG)
87
+ output_size: Number of output classes
88
+ base_channels: Base number of channels (default: 128)
89
+ kernel_size: Primary kernel size for residual blocks (default: 5)
90
+ kernel_size_stem: Kernel size for the stem convolution (default: 7)
91
+ dropout_rate: Dropout probability before classifier (default: 0.3)
92
+
93
+ Example:
94
+ >>> model = ResNetWang(input_channels=12, output_size=5)
95
+ >>> x = torch.randn(32, 12, 1000)
96
+ >>> output = model(x)
97
+ >>> print(output.shape)
98
+
99
+ >>> features = model.extract_features(x)
100
+ >>> print(features.shape) # (32, 128)
101
+ """
102
+
103
+ def __init__(
104
+ self,
105
+ input_channels: int = 12,
106
+ output_size: int = 5,
107
+ base_channels: int = 128,
108
+ kernel_size: int = 5,
109
+ kernel_size_stem: int = 7,
110
+ dropout_rate: float = 0.3,
111
+ ):
112
+ super().__init__()
113
+
114
+ self.in_channels = base_channels
115
+
116
+ self.stem = nn.Sequential(
117
+ nn.Conv1d(
118
+ input_channels,
119
+ base_channels,
120
+ kernel_size=kernel_size_stem,
121
+ stride=1,
122
+ padding=(kernel_size_stem - 1) // 2,
123
+ bias=False,
124
+ ),
125
+ nn.BatchNorm1d(base_channels),
126
+ nn.ReLU(inplace=True),
127
+ )
128
+
129
+ self.layer1 = self._make_layer(base_channels, 1, kernel_size=kernel_size, stride=1)
130
+ self.layer2 = self._make_layer(base_channels, 1, kernel_size=kernel_size, stride=1)
131
+ self.layer3 = self._make_layer(base_channels, 1, kernel_size=kernel_size, stride=1)
132
+
133
+ self.global_pool = nn.AdaptiveAvgPool1d(1)
134
+ self.dropout = nn.Dropout(dropout_rate)
135
+ self._feature_dim = base_channels
136
+ self.classifier = nn.Linear(self._feature_dim, output_size)
137
+
138
+ def _make_layer(
139
+ self,
140
+ out_channels: int,
141
+ num_blocks: int,
142
+ kernel_size: int = 5,
143
+ stride: int = 1,
144
+ ) -> nn.Sequential:
145
+ downsample = None
146
+ if stride != 1 or self.in_channels != out_channels:
147
+ downsample = nn.Sequential(
148
+ nn.Conv1d(self.in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
149
+ nn.BatchNorm1d(out_channels),
150
+ )
151
+
152
+ layers = [
153
+ BasicBlock1DWang(
154
+ self.in_channels,
155
+ out_channels,
156
+ kernel_size=kernel_size,
157
+ stride=stride,
158
+ downsample=downsample,
159
+ )
160
+ ]
161
+ self.in_channels = out_channels
162
+ for _ in range(1, num_blocks):
163
+ layers.append(BasicBlock1DWang(out_channels, out_channels, kernel_size=kernel_size))
164
+
165
+ return nn.Sequential(*layers)
166
+
167
+ @property
168
+ def feature_dim(self) -> int:
169
+ return self._feature_dim
170
+
171
+ def extract_features(self, x: torch.Tensor) -> torch.Tensor:
172
+ x = self.stem(x)
173
+ x = self.layer1(x)
174
+ x = self.layer2(x)
175
+ x = self.layer3(x)
176
+ x = self.global_pool(x)
177
+ return torch.flatten(x, 1)
178
+
179
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
180
+ x = self.extract_features(x)
181
+ x = self.dropout(x)
182
+ return self.classifier(x)
@@ -0,0 +1,232 @@
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 SqueezeExcitation1D(nn.Module):
9
+ """
10
+ Squeeze-and-Excitation block for 1D signals.
11
+
12
+ Learns per-channel attention weights via global pooling followed by a
13
+ bottleneck MLP, allowing the network to emphasize informative channels
14
+ (e.g., specific ECG leads) and suppress less useful ones.
15
+
16
+ Args:
17
+ channels: Number of input/output channels
18
+ reduction: Channel reduction ratio for the bottleneck (default: 16)
19
+ """
20
+
21
+ def __init__(self, channels: int, reduction: int = 16):
22
+ super().__init__()
23
+ mid = max(channels // reduction, 1)
24
+ self.fc1 = nn.Linear(channels, mid, bias=False)
25
+ self.fc2 = nn.Linear(mid, channels, bias=False)
26
+
27
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
28
+ b, c, _ = x.shape
29
+ scale = x.mean(dim=2)
30
+ scale = func.relu(self.fc1(scale))
31
+ scale = torch.sigmoid(self.fc2(scale))
32
+ return x * scale.view(b, c, 1)
33
+
34
+
35
+ class SEResidualBlock1D(nn.Module):
36
+ """
37
+ Residual block with Squeeze-and-Excitation attention.
38
+
39
+ Args:
40
+ in_channels: Number of input channels
41
+ out_channels: Number of output channels
42
+ kernel_size: Convolution kernel size (default: 7)
43
+ stride: Convolution stride (default: 1)
44
+ downsample: Downsample layer for skip connection if needed
45
+ se_reduction: SE reduction ratio (default: 16)
46
+ dropout_rate: Dropout probability (default: 0.0)
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ in_channels: int,
52
+ out_channels: int,
53
+ kernel_size: int = 7,
54
+ stride: int = 1,
55
+ downsample: nn.Module | None = None,
56
+ se_reduction: int = 16,
57
+ dropout_rate: float = 0.0,
58
+ ):
59
+ super().__init__()
60
+
61
+ self.conv1 = nn.Conv1d(
62
+ in_channels,
63
+ out_channels,
64
+ kernel_size=kernel_size,
65
+ stride=stride,
66
+ padding=kernel_size // 2,
67
+ bias=False,
68
+ )
69
+ self.bn1 = nn.BatchNorm1d(out_channels)
70
+
71
+ self.conv2 = nn.Conv1d(
72
+ out_channels,
73
+ out_channels,
74
+ kernel_size=kernel_size,
75
+ stride=1,
76
+ padding=kernel_size // 2,
77
+ bias=False,
78
+ )
79
+ self.bn2 = nn.BatchNorm1d(out_channels)
80
+
81
+ self.se = SqueezeExcitation1D(out_channels, reduction=se_reduction)
82
+ self.downsample = downsample
83
+ self.dropout = nn.Dropout(dropout_rate) if dropout_rate > 0 else nn.Identity()
84
+
85
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
86
+ identity = x
87
+
88
+ out = func.relu(self.bn1(self.conv1(x)))
89
+ out = self.dropout(out)
90
+ out = self.bn2(self.conv2(out))
91
+ out = self.se(out)
92
+
93
+ if self.downsample is not None:
94
+ identity = self.downsample(x)
95
+
96
+ out += identity
97
+ out = func.relu(out)
98
+ return out
99
+
100
+
101
+ @register_model(
102
+ name="se-resnet",
103
+ description="ResNet with Squeeze-and-Excitation channel attention for ECG",
104
+ )
105
+ class SEResNet1D(nn.Module):
106
+ """
107
+ SE-ResNet model for ECG signal classification.
108
+
109
+ Extends ResNet1D with Squeeze-and-Excitation blocks that learn channel
110
+ attention weights. Particularly effective for multi-lead ECG where the
111
+ network can learn which leads are most informative for each class.
112
+
113
+ Args:
114
+ input_channels: Number of input channels (default: 1 for single-lead ECG)
115
+ output_size: Number of output classes
116
+ base_channels: Base number of channels (default: 64)
117
+ num_blocks: List of number of blocks in each layer (default: [2, 2, 2, 2])
118
+ se_reduction: SE reduction ratio (default: 16)
119
+ dropout_rate: Dropout probability (default: 0.3)
120
+
121
+ Example:
122
+ >>> model = SEResNet1D(input_channels=12, output_size=5)
123
+ >>> x = torch.randn(32, 12, 5000)
124
+ >>> output = model(x)
125
+ >>> print(output.shape)
126
+
127
+ >>> features = model.extract_features(x)
128
+ >>> print(features.shape) # (32, 512) with base_channels=64
129
+ """
130
+
131
+ def __init__(
132
+ self,
133
+ input_channels: int = 1,
134
+ output_size: int = 4,
135
+ base_channels: int = 64,
136
+ num_blocks: list | None = None,
137
+ se_reduction: int = 16,
138
+ dropout_rate: float = 0.3,
139
+ ):
140
+ super().__init__()
141
+
142
+ if num_blocks is None:
143
+ num_blocks = [2, 2, 2, 2]
144
+
145
+ self.in_channels = base_channels
146
+ self.se_reduction = se_reduction
147
+ self.dropout_rate = dropout_rate
148
+
149
+ self.conv1 = nn.Conv1d(
150
+ input_channels,
151
+ base_channels,
152
+ kernel_size=15,
153
+ stride=2,
154
+ padding=7,
155
+ bias=False,
156
+ )
157
+ self.bn1 = nn.BatchNorm1d(base_channels)
158
+ self.relu = nn.ReLU(inplace=True)
159
+ self.maxpool = nn.MaxPool1d(kernel_size=3, stride=2, padding=1)
160
+
161
+ self.layer1 = self._make_layer(base_channels, num_blocks[0], stride=1)
162
+ self.layer2 = self._make_layer(base_channels * 2, num_blocks[1], stride=2)
163
+ self.layer3 = self._make_layer(base_channels * 4, num_blocks[2], stride=2)
164
+ self.layer4 = self._make_layer(base_channels * 8, num_blocks[3], stride=2)
165
+
166
+ self.avgpool = nn.AdaptiveAvgPool1d(1)
167
+ self.dropout = nn.Dropout(dropout_rate)
168
+ self._feature_dim = base_channels * 8
169
+ self.fc = nn.Linear(self._feature_dim, output_size)
170
+
171
+ def _make_layer(self, out_channels: int, num_blocks: int, stride: int = 1):
172
+ downsample = None
173
+ if stride != 1 or self.in_channels != out_channels:
174
+ downsample = nn.Sequential(
175
+ nn.Conv1d(
176
+ self.in_channels,
177
+ out_channels,
178
+ kernel_size=1,
179
+ stride=stride,
180
+ bias=False,
181
+ ),
182
+ nn.BatchNorm1d(out_channels),
183
+ )
184
+
185
+ layers = [
186
+ SEResidualBlock1D(
187
+ self.in_channels,
188
+ out_channels,
189
+ stride=stride,
190
+ downsample=downsample,
191
+ se_reduction=self.se_reduction,
192
+ dropout_rate=self.dropout_rate,
193
+ )
194
+ ]
195
+ self.in_channels = out_channels
196
+
197
+ for _ in range(1, num_blocks):
198
+ layers.append(
199
+ SEResidualBlock1D(
200
+ out_channels,
201
+ out_channels,
202
+ se_reduction=self.se_reduction,
203
+ dropout_rate=self.dropout_rate,
204
+ )
205
+ )
206
+
207
+ return nn.Sequential(*layers)
208
+
209
+ @property
210
+ def feature_dim(self) -> int:
211
+ return self._feature_dim
212
+
213
+ def extract_features(self, x: torch.Tensor) -> torch.Tensor:
214
+ x = self.conv1(x)
215
+ x = self.bn1(x)
216
+ x = self.relu(x)
217
+ x = self.maxpool(x)
218
+
219
+ x = self.layer1(x)
220
+ x = self.layer2(x)
221
+ x = self.layer3(x)
222
+ x = self.layer4(x)
223
+
224
+ x = self.avgpool(x)
225
+ x = torch.flatten(x, 1)
226
+ return x
227
+
228
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
229
+ x = self.extract_features(x)
230
+ x = self.dropout(x)
231
+ x = self.fc(x)
232
+ return x
@@ -0,0 +1,82 @@
1
+ import torch
2
+ from torch import nn
3
+
4
+ from deepecgkit.registry import register_model
5
+
6
+
7
+ @register_model(
8
+ name="simple-cnn",
9
+ description="Lightweight CNN for fast inference",
10
+ )
11
+ class SimpleCNN(nn.Module):
12
+ """
13
+ Simple CNN model for ECG signal classification.
14
+
15
+ A straightforward convolutional neural network with pooling and dropout
16
+ layers for basic ECG classification tasks.
17
+
18
+ Args:
19
+ input_channels: Number of input channels (default: 1 for single-lead ECG)
20
+ output_size: Number of output classes
21
+ dropout_rate: Dropout probability (default: 0.3)
22
+
23
+ Example:
24
+ >>> model = SimpleCNN(input_channels=1, output_size=4)
25
+ >>> x = torch.randn(32, 1, 3000)
26
+ >>> output = model(x)
27
+ >>> print(output.shape)
28
+
29
+ >>> features = model.extract_features(x)
30
+ >>> print(features.shape) # (32, 256)
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ input_channels: int = 1,
36
+ output_size: int = 4,
37
+ dropout_rate: float = 0.3,
38
+ ):
39
+ super().__init__()
40
+
41
+ self.features = nn.Sequential(
42
+ nn.Conv1d(input_channels, 32, kernel_size=7, padding=3),
43
+ nn.BatchNorm1d(32),
44
+ nn.ReLU(),
45
+ nn.MaxPool1d(kernel_size=2),
46
+ nn.Dropout(dropout_rate),
47
+ nn.Conv1d(32, 64, kernel_size=5, padding=2),
48
+ nn.BatchNorm1d(64),
49
+ nn.ReLU(),
50
+ nn.MaxPool1d(kernel_size=2),
51
+ nn.Dropout(dropout_rate),
52
+ nn.Conv1d(64, 128, kernel_size=3, padding=1),
53
+ nn.BatchNorm1d(128),
54
+ nn.ReLU(),
55
+ nn.MaxPool1d(kernel_size=2),
56
+ nn.Dropout(dropout_rate),
57
+ nn.Conv1d(128, 256, kernel_size=3, padding=1),
58
+ nn.BatchNorm1d(256),
59
+ nn.ReLU(),
60
+ nn.AdaptiveAvgPool1d(1),
61
+ )
62
+
63
+ self.classifier = nn.Sequential(
64
+ nn.Flatten(),
65
+ nn.Linear(256, 128),
66
+ nn.ReLU(),
67
+ nn.Dropout(dropout_rate),
68
+ nn.Linear(128, output_size),
69
+ )
70
+
71
+ @property
72
+ def feature_dim(self) -> int:
73
+ return 256
74
+
75
+ def extract_features(self, x: torch.Tensor) -> torch.Tensor:
76
+ x = self.features(x)
77
+ return torch.flatten(x, 1)
78
+
79
+ def forward(self, x):
80
+ x = self.features(x)
81
+ x = self.classifier(x)
82
+ return x
@@ -0,0 +1,178 @@
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 CausalConv1d(nn.Module):
9
+ """
10
+ Causal 1D convolution that only looks at past and present timesteps.
11
+
12
+ Uses left-side padding to ensure no information leakage from the future,
13
+ making it suitable for real-time or streaming ECG inference.
14
+
15
+ Args:
16
+ in_channels: Number of input channels
17
+ out_channels: Number of output channels
18
+ kernel_size: Convolution kernel size
19
+ dilation: Dilation factor for dilated convolutions
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ in_channels: int,
25
+ out_channels: int,
26
+ kernel_size: int,
27
+ dilation: int = 1,
28
+ ):
29
+ super().__init__()
30
+ self.padding = (kernel_size - 1) * dilation
31
+ self.conv = nn.Conv1d(
32
+ in_channels,
33
+ out_channels,
34
+ kernel_size=kernel_size,
35
+ dilation=dilation,
36
+ padding=self.padding,
37
+ bias=False,
38
+ )
39
+
40
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
41
+ out = self.conv(x)
42
+ if self.padding > 0:
43
+ out = out[:, :, : -self.padding]
44
+ return out
45
+
46
+
47
+ class TemporalBlock(nn.Module):
48
+ """
49
+ Single temporal block with dilated causal convolutions and residual connection.
50
+
51
+ Each block applies two dilated causal convolutions with weight normalization,
52
+ ReLU activations, and dropout, plus a 1x1 shortcut if channel sizes differ.
53
+
54
+ Args:
55
+ in_channels: Number of input channels
56
+ out_channels: Number of output channels
57
+ kernel_size: Convolution kernel size (default: 7)
58
+ dilation: Dilation factor (default: 1)
59
+ dropout_rate: Dropout probability (default: 0.2)
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ in_channels: int,
65
+ out_channels: int,
66
+ kernel_size: int = 7,
67
+ dilation: int = 1,
68
+ dropout_rate: float = 0.2,
69
+ ):
70
+ super().__init__()
71
+
72
+ self.causal_conv1 = CausalConv1d(in_channels, out_channels, kernel_size, dilation)
73
+ nn.utils.parametrizations.weight_norm(self.causal_conv1.conv)
74
+ self.bn1 = nn.BatchNorm1d(out_channels)
75
+ self.dropout1 = nn.Dropout(dropout_rate)
76
+
77
+ self.causal_conv2 = CausalConv1d(out_channels, out_channels, kernel_size, dilation)
78
+ nn.utils.parametrizations.weight_norm(self.causal_conv2.conv)
79
+ self.bn2 = nn.BatchNorm1d(out_channels)
80
+ self.dropout2 = nn.Dropout(dropout_rate)
81
+
82
+ self.shortcut = (
83
+ nn.Conv1d(in_channels, out_channels, kernel_size=1, bias=False)
84
+ if in_channels != out_channels
85
+ else nn.Identity()
86
+ )
87
+
88
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
89
+ out = self.causal_conv1(x)
90
+ out = func.relu(self.bn1(out))
91
+ out = self.dropout1(out)
92
+
93
+ out = self.causal_conv2(out)
94
+ out = func.relu(self.bn2(out))
95
+ out = self.dropout2(out)
96
+
97
+ return func.relu(out + self.shortcut(x))
98
+
99
+
100
+ @register_model(
101
+ name="tcn",
102
+ description="Temporal Convolutional Network with dilated causal convolutions",
103
+ )
104
+ class TCN(nn.Module):
105
+ """
106
+ Temporal Convolutional Network for ECG signal classification.
107
+
108
+ Uses stacked dilated causal convolutions with exponentially growing
109
+ receptive fields to efficiently model long-range dependencies without
110
+ recurrence. The causal structure makes it suitable for real-time
111
+ ECG monitoring applications.
112
+
113
+ Args:
114
+ input_channels: Number of input channels (default: 1 for single-lead ECG)
115
+ output_size: Number of output classes
116
+ num_channels: List of channel sizes per temporal block
117
+ (default: [64, 64, 128, 128, 256, 256])
118
+ kernel_size: Convolution kernel size (default: 7)
119
+ dropout_rate: Dropout probability (default: 0.2)
120
+
121
+ Example:
122
+ >>> model = TCN(input_channels=1, output_size=4)
123
+ >>> x = torch.randn(32, 1, 3000)
124
+ >>> output = model(x)
125
+ >>> print(output.shape)
126
+
127
+ >>> features = model.extract_features(x)
128
+ >>> print(features.shape) # (32, 256) with default num_channels
129
+ """
130
+
131
+ def __init__(
132
+ self,
133
+ input_channels: int = 1,
134
+ output_size: int = 4,
135
+ num_channels: list | None = None,
136
+ kernel_size: int = 7,
137
+ dropout_rate: float = 0.2,
138
+ ):
139
+ super().__init__()
140
+
141
+ if num_channels is None:
142
+ num_channels = [64, 64, 128, 128, 256, 256]
143
+
144
+ blocks = []
145
+ for i, out_ch in enumerate(num_channels):
146
+ in_ch = input_channels if i == 0 else num_channels[i - 1]
147
+ dilation = 2**i
148
+ blocks.append(
149
+ TemporalBlock(
150
+ in_channels=in_ch,
151
+ out_channels=out_ch,
152
+ kernel_size=kernel_size,
153
+ dilation=dilation,
154
+ dropout_rate=dropout_rate,
155
+ )
156
+ )
157
+
158
+ self.network = nn.Sequential(*blocks)
159
+ self.global_pool = nn.AdaptiveAvgPool1d(1)
160
+ self._feature_dim = num_channels[-1]
161
+ self.dropout = nn.Dropout(dropout_rate)
162
+ self.classifier = nn.Linear(self._feature_dim, output_size)
163
+
164
+ @property
165
+ def feature_dim(self) -> int:
166
+ return self._feature_dim
167
+
168
+ def extract_features(self, x: torch.Tensor) -> torch.Tensor:
169
+ x = self.network(x)
170
+ x = self.global_pool(x)
171
+ x = torch.flatten(x, 1)
172
+ return x
173
+
174
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
175
+ x = self.extract_features(x)
176
+ x = self.dropout(x)
177
+ x = self.classifier(x)
178
+ return x