deepecgkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepecgkit/__init__.py +42 -0
- deepecgkit/__main__.py +6 -0
- deepecgkit/cli/__init__.py +61 -0
- deepecgkit/cli/commands/__init__.py +16 -0
- deepecgkit/cli/commands/evaluate.py +83 -0
- deepecgkit/cli/commands/info.py +73 -0
- deepecgkit/cli/commands/predict.py +94 -0
- deepecgkit/cli/commands/train.py +331 -0
- deepecgkit/cli/config.py +23 -0
- deepecgkit/cli/evaluation.py +251 -0
- deepecgkit/cli/logger.py +26 -0
- deepecgkit/cli/main.py +429 -0
- deepecgkit/cli/registry.py +32 -0
- deepecgkit/datasets/__init__.py +33 -0
- deepecgkit/datasets/af_classification.py +301 -0
- deepecgkit/datasets/base.py +146 -0
- deepecgkit/datasets/ltafdb.py +363 -0
- deepecgkit/datasets/mitbih_afdb.py +287 -0
- deepecgkit/datasets/modules.py +222 -0
- deepecgkit/datasets/preprocessing.py +208 -0
- deepecgkit/datasets/ptbxl.py +567 -0
- deepecgkit/datasets/splitting.py +225 -0
- deepecgkit/datasets/unified_af.py +316 -0
- deepecgkit/evaluation/__init__.py +40 -0
- deepecgkit/evaluation/evaluator.py +398 -0
- deepecgkit/evaluation/metrics.py +164 -0
- deepecgkit/evaluation/visualization.py +285 -0
- deepecgkit/models/__init__.py +51 -0
- deepecgkit/models/af_classifier.py +177 -0
- deepecgkit/models/convnext_v2_1d.py +183 -0
- deepecgkit/models/crnn.py +117 -0
- deepecgkit/models/deep_res_cnn.py +215 -0
- deepecgkit/models/ecg_dualnet.py +230 -0
- deepecgkit/models/fcn_wang.py +88 -0
- deepecgkit/models/gru_model.py +99 -0
- deepecgkit/models/inception_time.py +197 -0
- deepecgkit/models/kanres_wide_x.py +150 -0
- deepecgkit/models/kanres_x.py +126 -0
- deepecgkit/models/lstm_model.py +96 -0
- deepecgkit/models/mamba1d.py +232 -0
- deepecgkit/models/medformer.py +229 -0
- deepecgkit/models/resnet1d.py +189 -0
- deepecgkit/models/resnet1d_wang.py +182 -0
- deepecgkit/models/se_resnet1d.py +232 -0
- deepecgkit/models/simple_cnn.py +82 -0
- deepecgkit/models/tcn.py +178 -0
- deepecgkit/models/transformer_ecg.py +127 -0
- deepecgkit/models/xresnet1d.py +265 -0
- deepecgkit/models/xresnet1d_benchmark.py +227 -0
- deepecgkit/registry.py +97 -0
- deepecgkit/training/__init__.py +9 -0
- deepecgkit/training/train.py +171 -0
- deepecgkit/utils/__init__.py +72 -0
- deepecgkit/utils/download.py +123 -0
- deepecgkit/utils/weights.py +107 -0
- deepecgkit-0.1.0.dist-info/METADATA +99 -0
- deepecgkit-0.1.0.dist-info/RECORD +61 -0
- deepecgkit-0.1.0.dist-info/WHEEL +5 -0
- deepecgkit-0.1.0.dist-info/entry_points.txt +2 -0
- deepecgkit-0.1.0.dist-info/licenses/LICENSE +201 -0
- deepecgkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
deepecgkit/models/tcn.py
ADDED
|
@@ -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
|