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,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)
|