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,127 @@
|
|
|
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
|
+
Positional encoding for transformer model.
|
|
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):
|
|
31
|
+
x = x + self.pe[:, : x.size(1)]
|
|
32
|
+
return self.dropout(x)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@register_model(
|
|
36
|
+
name="transformer",
|
|
37
|
+
description="Transformer-based ECG classifier",
|
|
38
|
+
)
|
|
39
|
+
class TransformerECG(nn.Module):
|
|
40
|
+
"""
|
|
41
|
+
Transformer-based model for ECG signal classification.
|
|
42
|
+
|
|
43
|
+
A transformer architecture that uses self-attention mechanisms
|
|
44
|
+
to capture long-range dependencies in ECG signals.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
input_channels: Number of input channels (default: 1 for single-lead ECG)
|
|
48
|
+
output_size: Number of output classes
|
|
49
|
+
d_model: Dimension of the model (default: 128)
|
|
50
|
+
nhead: Number of attention heads (default: 8)
|
|
51
|
+
num_encoder_layers: Number of transformer encoder layers (default: 4)
|
|
52
|
+
dim_feedforward: Dimension of feedforward network (default: 512)
|
|
53
|
+
dropout_rate: Dropout probability (default: 0.1)
|
|
54
|
+
max_len: Maximum sequence length (default: 5000)
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
>>> model = TransformerECG(input_channels=1, output_size=4)
|
|
58
|
+
>>> x = torch.randn(32, 1, 3000)
|
|
59
|
+
>>> output = model(x)
|
|
60
|
+
>>> print(output.shape)
|
|
61
|
+
|
|
62
|
+
>>> features = model.extract_features(x)
|
|
63
|
+
>>> print(features.shape) # (32, 128) with d_model=128
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
input_channels: int = 1,
|
|
69
|
+
output_size: int = 4,
|
|
70
|
+
d_model: int = 128,
|
|
71
|
+
nhead: int = 8,
|
|
72
|
+
num_encoder_layers: int = 4,
|
|
73
|
+
dim_feedforward: int = 512,
|
|
74
|
+
dropout_rate: float = 0.1,
|
|
75
|
+
max_len: int = 5000,
|
|
76
|
+
):
|
|
77
|
+
super().__init__()
|
|
78
|
+
|
|
79
|
+
self.d_model = d_model
|
|
80
|
+
|
|
81
|
+
self.input_projection = nn.Linear(input_channels, d_model)
|
|
82
|
+
|
|
83
|
+
self.positional_encoding = PositionalEncoding(d_model, max_len, dropout_rate)
|
|
84
|
+
|
|
85
|
+
encoder_layer = nn.TransformerEncoderLayer(
|
|
86
|
+
d_model=d_model,
|
|
87
|
+
nhead=nhead,
|
|
88
|
+
dim_feedforward=dim_feedforward,
|
|
89
|
+
dropout=dropout_rate,
|
|
90
|
+
batch_first=True,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self.transformer_encoder = nn.TransformerEncoder(
|
|
94
|
+
encoder_layer,
|
|
95
|
+
num_layers=num_encoder_layers,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.global_pool = nn.AdaptiveAvgPool1d(1)
|
|
99
|
+
self._feature_dim = d_model
|
|
100
|
+
|
|
101
|
+
self.classifier = nn.Sequential(
|
|
102
|
+
nn.Linear(d_model, dim_feedforward // 2),
|
|
103
|
+
nn.ReLU(),
|
|
104
|
+
nn.Dropout(dropout_rate),
|
|
105
|
+
nn.Linear(dim_feedforward // 2, output_size),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def feature_dim(self) -> int:
|
|
110
|
+
return self._feature_dim
|
|
111
|
+
|
|
112
|
+
def _extract_encoder_features(self, x: torch.Tensor) -> torch.Tensor:
|
|
113
|
+
x = x.transpose(1, 2)
|
|
114
|
+
x = self.input_projection(x)
|
|
115
|
+
x = self.positional_encoding(x)
|
|
116
|
+
x = self.transformer_encoder(x)
|
|
117
|
+
x = x.transpose(1, 2)
|
|
118
|
+
x = self.global_pool(x)
|
|
119
|
+
x = x.squeeze(-1)
|
|
120
|
+
return x
|
|
121
|
+
|
|
122
|
+
def extract_features(self, x: torch.Tensor) -> torch.Tensor:
|
|
123
|
+
return self._extract_encoder_features(x)
|
|
124
|
+
|
|
125
|
+
def forward(self, x):
|
|
126
|
+
x = self.extract_features(x)
|
|
127
|
+
return self.classifier(x)
|
|
@@ -0,0 +1,265 @@
|
|
|
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 Mish(nn.Module):
|
|
9
|
+
"""Mish activation: x * tanh(softplus(x))."""
|
|
10
|
+
|
|
11
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
12
|
+
return x * torch.tanh(func.softplus(x))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BlurPool1D(nn.Module):
|
|
16
|
+
"""
|
|
17
|
+
Anti-aliased downsampling via blur-then-subsample.
|
|
18
|
+
|
|
19
|
+
Applies a fixed triangular low-pass filter before strided subsampling
|
|
20
|
+
to reduce aliasing artifacts during spatial reduction.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
channels: Number of channels (applied per-channel)
|
|
24
|
+
stride: Downsampling stride (default: 2)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, channels: int, stride: int = 2):
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.stride = stride
|
|
30
|
+
kernel = torch.tensor([1.0, 2.0, 1.0]) / 4.0
|
|
31
|
+
kernel = kernel.unsqueeze(0).unsqueeze(0).repeat(channels, 1, 1)
|
|
32
|
+
self.register_buffer("kernel", kernel)
|
|
33
|
+
self.channels = channels
|
|
34
|
+
|
|
35
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
36
|
+
x = func.pad(x, (1, 1), mode="reflect")
|
|
37
|
+
x = func.conv1d(x, self.kernel, stride=self.stride, groups=self.channels)
|
|
38
|
+
return x
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class XResBlock1D(nn.Module):
|
|
42
|
+
"""
|
|
43
|
+
Improved residual block with Mish activation and optional blur-pool downsampling.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
in_channels: Number of input channels
|
|
47
|
+
out_channels: Number of output channels
|
|
48
|
+
kernel_size: Convolution kernel size (default: 7)
|
|
49
|
+
stride: Convolution stride (default: 1)
|
|
50
|
+
downsample: Downsample layer for skip connection if needed
|
|
51
|
+
use_blur_pool: Whether to use anti-aliased downsampling
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
in_channels: int,
|
|
57
|
+
out_channels: int,
|
|
58
|
+
kernel_size: int = 7,
|
|
59
|
+
stride: int = 1,
|
|
60
|
+
downsample: nn.Module | None = None,
|
|
61
|
+
use_blur_pool: bool = True,
|
|
62
|
+
):
|
|
63
|
+
super().__init__()
|
|
64
|
+
|
|
65
|
+
self.conv1 = nn.Conv1d(
|
|
66
|
+
in_channels,
|
|
67
|
+
out_channels,
|
|
68
|
+
kernel_size=kernel_size,
|
|
69
|
+
stride=1,
|
|
70
|
+
padding=kernel_size // 2,
|
|
71
|
+
bias=False,
|
|
72
|
+
)
|
|
73
|
+
self.bn1 = nn.BatchNorm1d(out_channels)
|
|
74
|
+
|
|
75
|
+
self.conv2 = nn.Conv1d(
|
|
76
|
+
out_channels,
|
|
77
|
+
out_channels,
|
|
78
|
+
kernel_size=kernel_size,
|
|
79
|
+
stride=1,
|
|
80
|
+
padding=kernel_size // 2,
|
|
81
|
+
bias=False,
|
|
82
|
+
)
|
|
83
|
+
self.bn2 = nn.BatchNorm1d(out_channels)
|
|
84
|
+
|
|
85
|
+
self.act = Mish()
|
|
86
|
+
self.downsample = downsample
|
|
87
|
+
|
|
88
|
+
if stride > 1 and use_blur_pool:
|
|
89
|
+
self.pool = BlurPool1D(out_channels, stride=stride)
|
|
90
|
+
elif stride > 1:
|
|
91
|
+
self.pool = nn.AvgPool1d(kernel_size=stride, stride=stride)
|
|
92
|
+
else:
|
|
93
|
+
self.pool = nn.Identity()
|
|
94
|
+
|
|
95
|
+
nn.init.zeros_(self.bn2.weight)
|
|
96
|
+
|
|
97
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
98
|
+
identity = x
|
|
99
|
+
|
|
100
|
+
out = self.act(self.bn1(self.conv1(x)))
|
|
101
|
+
out = self.bn2(self.conv2(out))
|
|
102
|
+
out = self.pool(out)
|
|
103
|
+
|
|
104
|
+
if self.downsample is not None:
|
|
105
|
+
identity = self.downsample(x)
|
|
106
|
+
|
|
107
|
+
out += identity
|
|
108
|
+
out = self.act(out)
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@register_model(
|
|
113
|
+
name="xresnet",
|
|
114
|
+
description="Improved ResNet with Mish activation and blur-pool downsampling",
|
|
115
|
+
)
|
|
116
|
+
class XResNet1D(nn.Module):
|
|
117
|
+
"""
|
|
118
|
+
XResNet model for ECG signal classification.
|
|
119
|
+
|
|
120
|
+
An improved ResNet incorporating three key enhancements from recent
|
|
121
|
+
research: (1) a multi-layer stem instead of a single large convolution,
|
|
122
|
+
(2) Mish activation for smoother gradients, and (3) anti-aliased
|
|
123
|
+
blur-pool downsampling to reduce aliasing.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
input_channels: Number of input channels (default: 1 for single-lead ECG)
|
|
127
|
+
output_size: Number of output classes
|
|
128
|
+
base_channels: Base number of channels (default: 64)
|
|
129
|
+
num_blocks: List of number of blocks in each layer (default: [2, 2, 2, 2])
|
|
130
|
+
dropout_rate: Dropout probability (default: 0.3)
|
|
131
|
+
use_blur_pool: Whether to use anti-aliased downsampling (default: True)
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
>>> model = XResNet1D(input_channels=1, output_size=4)
|
|
135
|
+
>>> x = torch.randn(32, 1, 3000)
|
|
136
|
+
>>> output = model(x)
|
|
137
|
+
>>> print(output.shape)
|
|
138
|
+
|
|
139
|
+
>>> features = model.extract_features(x)
|
|
140
|
+
>>> print(features.shape) # (32, 512) with base_channels=64
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(
|
|
144
|
+
self,
|
|
145
|
+
input_channels: int = 1,
|
|
146
|
+
output_size: int = 4,
|
|
147
|
+
base_channels: int = 64,
|
|
148
|
+
num_blocks: list | None = None,
|
|
149
|
+
dropout_rate: float = 0.3,
|
|
150
|
+
use_blur_pool: bool = True,
|
|
151
|
+
):
|
|
152
|
+
super().__init__()
|
|
153
|
+
|
|
154
|
+
if num_blocks is None:
|
|
155
|
+
num_blocks = [2, 2, 2, 2]
|
|
156
|
+
|
|
157
|
+
self.in_channels = base_channels
|
|
158
|
+
self.use_blur_pool = use_blur_pool
|
|
159
|
+
self.act = Mish()
|
|
160
|
+
|
|
161
|
+
self.stem = nn.Sequential(
|
|
162
|
+
nn.Conv1d(
|
|
163
|
+
input_channels,
|
|
164
|
+
base_channels // 2,
|
|
165
|
+
kernel_size=7,
|
|
166
|
+
stride=2,
|
|
167
|
+
padding=3,
|
|
168
|
+
bias=False,
|
|
169
|
+
),
|
|
170
|
+
nn.BatchNorm1d(base_channels // 2),
|
|
171
|
+
Mish(),
|
|
172
|
+
nn.Conv1d(
|
|
173
|
+
base_channels // 2,
|
|
174
|
+
base_channels // 2,
|
|
175
|
+
kernel_size=3,
|
|
176
|
+
stride=1,
|
|
177
|
+
padding=1,
|
|
178
|
+
bias=False,
|
|
179
|
+
),
|
|
180
|
+
nn.BatchNorm1d(base_channels // 2),
|
|
181
|
+
Mish(),
|
|
182
|
+
nn.Conv1d(
|
|
183
|
+
base_channels // 2,
|
|
184
|
+
base_channels,
|
|
185
|
+
kernel_size=3,
|
|
186
|
+
stride=1,
|
|
187
|
+
padding=1,
|
|
188
|
+
bias=False,
|
|
189
|
+
),
|
|
190
|
+
nn.BatchNorm1d(base_channels),
|
|
191
|
+
Mish(),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if use_blur_pool:
|
|
195
|
+
self.stem_pool = BlurPool1D(base_channels, stride=2)
|
|
196
|
+
else:
|
|
197
|
+
self.stem_pool = nn.MaxPool1d(kernel_size=3, stride=2, padding=1)
|
|
198
|
+
|
|
199
|
+
self.layer1 = self._make_layer(base_channels, num_blocks[0], stride=1)
|
|
200
|
+
self.layer2 = self._make_layer(base_channels * 2, num_blocks[1], stride=2)
|
|
201
|
+
self.layer3 = self._make_layer(base_channels * 4, num_blocks[2], stride=2)
|
|
202
|
+
self.layer4 = self._make_layer(base_channels * 8, num_blocks[3], stride=2)
|
|
203
|
+
|
|
204
|
+
self.avgpool = nn.AdaptiveAvgPool1d(1)
|
|
205
|
+
self.dropout = nn.Dropout(dropout_rate)
|
|
206
|
+
self._feature_dim = base_channels * 8
|
|
207
|
+
self.fc = nn.Linear(self._feature_dim, output_size)
|
|
208
|
+
|
|
209
|
+
def _make_layer(self, out_channels: int, num_blocks: int, stride: int = 1):
|
|
210
|
+
downsample = None
|
|
211
|
+
if stride != 1 or self.in_channels != out_channels:
|
|
212
|
+
ds_layers: list[nn.Module] = []
|
|
213
|
+
if stride > 1:
|
|
214
|
+
if self.use_blur_pool:
|
|
215
|
+
ds_layers.append(BlurPool1D(self.in_channels, stride=stride))
|
|
216
|
+
else:
|
|
217
|
+
ds_layers.append(nn.AvgPool1d(kernel_size=stride, stride=stride))
|
|
218
|
+
ds_layers.append(nn.Conv1d(self.in_channels, out_channels, kernel_size=1, bias=False))
|
|
219
|
+
ds_layers.append(nn.BatchNorm1d(out_channels))
|
|
220
|
+
downsample = nn.Sequential(*ds_layers)
|
|
221
|
+
|
|
222
|
+
layers = [
|
|
223
|
+
XResBlock1D(
|
|
224
|
+
self.in_channels,
|
|
225
|
+
out_channels,
|
|
226
|
+
stride=stride,
|
|
227
|
+
downsample=downsample,
|
|
228
|
+
use_blur_pool=self.use_blur_pool,
|
|
229
|
+
)
|
|
230
|
+
]
|
|
231
|
+
self.in_channels = out_channels
|
|
232
|
+
|
|
233
|
+
for _ in range(1, num_blocks):
|
|
234
|
+
layers.append(
|
|
235
|
+
XResBlock1D(
|
|
236
|
+
out_channels,
|
|
237
|
+
out_channels,
|
|
238
|
+
use_blur_pool=self.use_blur_pool,
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return nn.Sequential(*layers)
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def feature_dim(self) -> int:
|
|
246
|
+
return self._feature_dim
|
|
247
|
+
|
|
248
|
+
def extract_features(self, x: torch.Tensor) -> torch.Tensor:
|
|
249
|
+
x = self.stem(x)
|
|
250
|
+
x = self.stem_pool(x)
|
|
251
|
+
|
|
252
|
+
x = self.layer1(x)
|
|
253
|
+
x = self.layer2(x)
|
|
254
|
+
x = self.layer3(x)
|
|
255
|
+
x = self.layer4(x)
|
|
256
|
+
|
|
257
|
+
x = self.avgpool(x)
|
|
258
|
+
x = torch.flatten(x, 1)
|
|
259
|
+
return x
|
|
260
|
+
|
|
261
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
262
|
+
x = self.extract_features(x)
|
|
263
|
+
x = self.dropout(x)
|
|
264
|
+
x = self.fc(x)
|
|
265
|
+
return x
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch import nn
|
|
3
|
+
|
|
4
|
+
from deepecgkit.registry import register_model
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _init_cnn(m: nn.Module):
|
|
8
|
+
if getattr(m, "bias", None) is not None:
|
|
9
|
+
nn.init.constant_(m.bias, 0)
|
|
10
|
+
if isinstance(m, (nn.Conv1d, nn.Linear)):
|
|
11
|
+
nn.init.kaiming_normal_(m.weight)
|
|
12
|
+
for child in m.children():
|
|
13
|
+
_init_cnn(child)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConvBnAct(nn.Sequential):
|
|
17
|
+
"""Conv1d → BatchNorm1d → Activation."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
ni: int,
|
|
22
|
+
nf: int,
|
|
23
|
+
ks: int = 3,
|
|
24
|
+
stride: int = 1,
|
|
25
|
+
act: bool = True,
|
|
26
|
+
zero_bn: bool = False,
|
|
27
|
+
):
|
|
28
|
+
layers: list[nn.Module] = [
|
|
29
|
+
nn.Conv1d(ni, nf, kernel_size=ks, stride=stride, padding=(ks - 1) // 2, bias=False),
|
|
30
|
+
]
|
|
31
|
+
bn = nn.BatchNorm1d(nf)
|
|
32
|
+
if zero_bn:
|
|
33
|
+
nn.init.zeros_(bn.weight)
|
|
34
|
+
layers.append(bn)
|
|
35
|
+
if act:
|
|
36
|
+
layers.append(nn.ReLU(inplace=True))
|
|
37
|
+
super().__init__(*layers)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class XResBlock(nn.Module):
|
|
41
|
+
"""
|
|
42
|
+
XResNet residual block with expansion support.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
expansion: Channel expansion factor (1 for BasicBlock, 4 for Bottleneck)
|
|
46
|
+
ni: Input channels (before expansion)
|
|
47
|
+
nf: Output channels (before expansion)
|
|
48
|
+
stride: Stride for downsampling
|
|
49
|
+
kernel_size: Convolution kernel size
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, expansion: int, ni: int, nf: int, stride: int = 1, kernel_size: int = 5):
|
|
53
|
+
super().__init__()
|
|
54
|
+
nf_out = nf * expansion
|
|
55
|
+
ni_in = ni * expansion
|
|
56
|
+
|
|
57
|
+
if expansion == 1:
|
|
58
|
+
self.convs = nn.Sequential(
|
|
59
|
+
ConvBnAct(ni_in, nf, ks=kernel_size, stride=stride),
|
|
60
|
+
ConvBnAct(nf, nf_out, ks=kernel_size, zero_bn=True, act=False),
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
self.convs = nn.Sequential(
|
|
64
|
+
ConvBnAct(ni_in, nf, ks=1),
|
|
65
|
+
ConvBnAct(nf, nf, ks=kernel_size, stride=stride),
|
|
66
|
+
ConvBnAct(nf, nf_out, ks=1, zero_bn=True, act=False),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
id_layers: list[nn.Module] = []
|
|
70
|
+
if ni_in != nf_out:
|
|
71
|
+
id_layers.append(ConvBnAct(ni_in, nf_out, ks=1, act=False))
|
|
72
|
+
if stride != 1:
|
|
73
|
+
id_layers.insert(0, nn.AvgPool1d(2, ceil_mode=True))
|
|
74
|
+
self.idpath = nn.Sequential(*id_layers)
|
|
75
|
+
self.act = nn.ReLU(inplace=True)
|
|
76
|
+
|
|
77
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
78
|
+
return self.act(self.convs(x) + self.idpath(x))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@register_model(
|
|
82
|
+
name="xresnet1d-benchmark",
|
|
83
|
+
description="XResNet1d from PTB-XL benchmark (fixed feature dim, concat pooling)",
|
|
84
|
+
)
|
|
85
|
+
class XResNet1dBenchmark(nn.Module):
|
|
86
|
+
"""
|
|
87
|
+
XResNet1d adapted from the PTB-XL benchmarking repository.
|
|
88
|
+
|
|
89
|
+
Key differences from the standard XResNet1D:
|
|
90
|
+
- Fixed feature dimension across all layers (all blocks use same width)
|
|
91
|
+
- Concat pooling head (AdaptiveAvgPool + AdaptiveMaxPool concatenated)
|
|
92
|
+
- Multi-layer stem with configurable kernel size
|
|
93
|
+
- Supports both BasicBlock (expansion=1) and Bottleneck (expansion=4)
|
|
94
|
+
|
|
95
|
+
Reference:
|
|
96
|
+
Strodthoff N., Wagner P., Schaeffter T., Samek W.
|
|
97
|
+
"Deep Learning for ECG Analysis: Benchmarks and Insights from PTB-XL" (2021)
|
|
98
|
+
https://github.com/helme/ecg_ptbxl_benchmarking
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
input_channels: Number of input channels (default: 12 for 12-lead ECG)
|
|
102
|
+
output_size: Number of output classes
|
|
103
|
+
expansion: Block expansion factor (1=BasicBlock, 4=Bottleneck)
|
|
104
|
+
layers: List of block counts per layer (default: [3, 4, 6, 3] for ResNet-50)
|
|
105
|
+
base_channels: Fixed channel width for all layers (default: 64)
|
|
106
|
+
kernel_size: Convolution kernel size (default: 5)
|
|
107
|
+
kernel_size_stem: Stem convolution kernel size (default: 5)
|
|
108
|
+
stem_channels: Tuple of stem layer channels
|
|
109
|
+
dropout_rate: Dropout probability in head (default: 0.5)
|
|
110
|
+
concat_pooling: Use avg+max concat pooling (default: True)
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
>>> model = XResNet1dBenchmark(input_channels=12, output_size=5)
|
|
114
|
+
>>> x = torch.randn(32, 12, 1000)
|
|
115
|
+
>>> output = model(x)
|
|
116
|
+
>>> print(output.shape)
|
|
117
|
+
|
|
118
|
+
>>> features = model.extract_features(x)
|
|
119
|
+
>>> print(features.shape) # (32, 128) with concat_pooling
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
input_channels: int = 12,
|
|
125
|
+
output_size: int = 5,
|
|
126
|
+
expansion: int = 1,
|
|
127
|
+
layers: list[int] | None = None,
|
|
128
|
+
base_channels: int = 64,
|
|
129
|
+
kernel_size: int = 5,
|
|
130
|
+
kernel_size_stem: int = 5,
|
|
131
|
+
stem_channels: tuple[int, ...] = (32, 32, 64),
|
|
132
|
+
dropout_rate: float = 0.5,
|
|
133
|
+
concat_pooling: bool = True,
|
|
134
|
+
):
|
|
135
|
+
super().__init__()
|
|
136
|
+
|
|
137
|
+
if layers is None:
|
|
138
|
+
layers = [2, 2, 2, 2]
|
|
139
|
+
|
|
140
|
+
self.expansion = expansion
|
|
141
|
+
self.concat_pooling = concat_pooling
|
|
142
|
+
|
|
143
|
+
stem_szs = [input_channels, *stem_channels]
|
|
144
|
+
self.stem = nn.Sequential(
|
|
145
|
+
*[
|
|
146
|
+
ConvBnAct(
|
|
147
|
+
stem_szs[i], stem_szs[i + 1], ks=kernel_size_stem, stride=2 if i == 0 else 1
|
|
148
|
+
)
|
|
149
|
+
for i in range(len(stem_channels))
|
|
150
|
+
]
|
|
151
|
+
)
|
|
152
|
+
self.stem_pool = nn.MaxPool1d(kernel_size=3, stride=2, padding=1)
|
|
153
|
+
|
|
154
|
+
block_szs = [64 // expansion, *([base_channels] * len(layers))]
|
|
155
|
+
|
|
156
|
+
self.res_layers = nn.Sequential(
|
|
157
|
+
*[
|
|
158
|
+
self._make_layer(
|
|
159
|
+
ni=block_szs[i],
|
|
160
|
+
nf=block_szs[i + 1],
|
|
161
|
+
blocks=n_blocks,
|
|
162
|
+
stride=1 if i == 0 else 2,
|
|
163
|
+
kernel_size=kernel_size,
|
|
164
|
+
)
|
|
165
|
+
for i, n_blocks in enumerate(layers)
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
final_nf = block_szs[-1] * expansion
|
|
170
|
+
self._feature_dim = final_nf * 2 if concat_pooling else final_nf
|
|
171
|
+
|
|
172
|
+
if concat_pooling:
|
|
173
|
+
self.pool = nn.ModuleList([nn.AdaptiveAvgPool1d(1), nn.AdaptiveMaxPool1d(1)])
|
|
174
|
+
else:
|
|
175
|
+
self.pool = nn.ModuleList([nn.AdaptiveAvgPool1d(1)])
|
|
176
|
+
|
|
177
|
+
self.head = nn.Sequential(
|
|
178
|
+
nn.Flatten(),
|
|
179
|
+
nn.BatchNorm1d(self._feature_dim),
|
|
180
|
+
nn.Dropout(dropout_rate / 2),
|
|
181
|
+
nn.Linear(self._feature_dim, output_size),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
_init_cnn(self)
|
|
185
|
+
|
|
186
|
+
def _make_layer(
|
|
187
|
+
self, ni: int, nf: int, blocks: int, stride: int, kernel_size: int
|
|
188
|
+
) -> nn.Sequential:
|
|
189
|
+
return nn.Sequential(
|
|
190
|
+
*[
|
|
191
|
+
XResBlock(
|
|
192
|
+
self.expansion,
|
|
193
|
+
ni if i == 0 else nf,
|
|
194
|
+
nf,
|
|
195
|
+
stride=stride if i == 0 else 1,
|
|
196
|
+
kernel_size=kernel_size,
|
|
197
|
+
)
|
|
198
|
+
for i in range(blocks)
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def feature_dim(self) -> int:
|
|
204
|
+
return self._feature_dim
|
|
205
|
+
|
|
206
|
+
def extract_features(self, x: torch.Tensor) -> torch.Tensor:
|
|
207
|
+
x = self.stem(x)
|
|
208
|
+
x = self.stem_pool(x)
|
|
209
|
+
x = self.res_layers(x)
|
|
210
|
+
pooled = torch.cat([p(x) for p in self.pool], dim=1)
|
|
211
|
+
return torch.flatten(pooled, 1)
|
|
212
|
+
|
|
213
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
214
|
+
x = self.extract_features(x)
|
|
215
|
+
return self.head(x)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def xresnet1d18_benchmark(**kwargs) -> XResNet1dBenchmark:
|
|
219
|
+
return XResNet1dBenchmark(expansion=1, layers=[2, 2, 2, 2], **kwargs)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def xresnet1d50_benchmark(**kwargs) -> XResNet1dBenchmark:
|
|
223
|
+
return XResNet1dBenchmark(expansion=4, layers=[3, 4, 6, 3], **kwargs)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def xresnet1d101_benchmark(**kwargs) -> XResNet1dBenchmark:
|
|
227
|
+
return XResNet1dBenchmark(expansion=4, layers=[3, 4, 23, 3], **kwargs)
|