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,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 SelectiveSSM(nn.Module):
|
|
9
|
+
"""
|
|
10
|
+
Selective State Space Model implemented in pure PyTorch.
|
|
11
|
+
|
|
12
|
+
Implements a discretized state space: h_t = A_bar * h_{t-1} + B_bar * x_t,
|
|
13
|
+
y_t = C * h_t, where B, C, and delta are input-dependent (selective).
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
d_inner: Inner model dimension
|
|
17
|
+
d_state: State space dimension (default: 16)
|
|
18
|
+
d_conv: Local convolution width (default: 4)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, d_inner: int, d_state: int = 16, d_conv: int = 4):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.d_inner = d_inner
|
|
24
|
+
self.d_state = d_state
|
|
25
|
+
|
|
26
|
+
self.conv1d = nn.Conv1d(
|
|
27
|
+
d_inner,
|
|
28
|
+
d_inner,
|
|
29
|
+
kernel_size=d_conv,
|
|
30
|
+
groups=d_inner,
|
|
31
|
+
padding=d_conv - 1,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
self.x_proj = nn.Linear(d_inner, d_state * 2 + 1, bias=False)
|
|
35
|
+
self.dt_proj = nn.Linear(1, d_inner, bias=True)
|
|
36
|
+
|
|
37
|
+
a_log = torch.log(torch.arange(1, d_state + 1, dtype=torch.float32))
|
|
38
|
+
a_log = a_log.unsqueeze(0).expand(d_inner, -1)
|
|
39
|
+
self.a_log = nn.Parameter(a_log)
|
|
40
|
+
|
|
41
|
+
self.D = nn.Parameter(torch.ones(d_inner))
|
|
42
|
+
|
|
43
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
44
|
+
batch, seq_len, _ = x.shape
|
|
45
|
+
|
|
46
|
+
x_conv = x.transpose(1, 2)
|
|
47
|
+
x_conv = self.conv1d(x_conv)[:, :, :seq_len]
|
|
48
|
+
x_conv = func.silu(x_conv).transpose(1, 2)
|
|
49
|
+
|
|
50
|
+
proj = self.x_proj(x_conv)
|
|
51
|
+
b_sel = proj[:, :, : self.d_state]
|
|
52
|
+
c_sel = proj[:, :, self.d_state : self.d_state * 2]
|
|
53
|
+
delta = proj[:, :, self.d_state * 2 :]
|
|
54
|
+
|
|
55
|
+
delta = func.softplus(self.dt_proj(delta))
|
|
56
|
+
|
|
57
|
+
a_coeff = -torch.exp(self.a_log)
|
|
58
|
+
|
|
59
|
+
outputs = []
|
|
60
|
+
h = torch.zeros(batch, self.d_inner, self.d_state, device=x.device, dtype=x.dtype)
|
|
61
|
+
|
|
62
|
+
for t in range(seq_len):
|
|
63
|
+
dt = delta[:, t].unsqueeze(-1)
|
|
64
|
+
a_bar = torch.exp(dt * a_coeff)
|
|
65
|
+
b_bar = dt * b_sel[:, t].unsqueeze(1)
|
|
66
|
+
x_t = x_conv[:, t].unsqueeze(-1)
|
|
67
|
+
|
|
68
|
+
h = a_bar * h + b_bar * x_t
|
|
69
|
+
y_t = (h * c_sel[:, t].unsqueeze(1)).sum(-1)
|
|
70
|
+
outputs.append(y_t)
|
|
71
|
+
|
|
72
|
+
y = torch.stack(outputs, dim=1)
|
|
73
|
+
return y + x * self.D
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class MambaBlock(nn.Module):
|
|
77
|
+
"""
|
|
78
|
+
Single Mamba block: norm -> linear expand -> SSM -> gated output -> project + residual.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
d_model: Model dimension
|
|
82
|
+
d_state: State space dimension (default: 16)
|
|
83
|
+
d_conv: Local convolution width (default: 4)
|
|
84
|
+
expansion_factor: Inner dimension expansion (default: 2)
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
d_model: int,
|
|
90
|
+
d_state: int = 16,
|
|
91
|
+
d_conv: int = 4,
|
|
92
|
+
expansion_factor: int = 2,
|
|
93
|
+
):
|
|
94
|
+
super().__init__()
|
|
95
|
+
d_inner = d_model * expansion_factor
|
|
96
|
+
|
|
97
|
+
self.norm = nn.LayerNorm(d_model)
|
|
98
|
+
self.in_proj = nn.Linear(d_model, d_inner * 2)
|
|
99
|
+
self.ssm = SelectiveSSM(d_inner, d_state, d_conv)
|
|
100
|
+
self.out_proj = nn.Linear(d_inner, d_model)
|
|
101
|
+
|
|
102
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
103
|
+
residual = x
|
|
104
|
+
x = self.norm(x)
|
|
105
|
+
x, z = self.in_proj(x).chunk(2, dim=-1)
|
|
106
|
+
x = self.ssm(x)
|
|
107
|
+
x = x * func.silu(z)
|
|
108
|
+
x = self.out_proj(x)
|
|
109
|
+
return x + residual
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class BidirectionalMamba(nn.Module):
|
|
113
|
+
"""
|
|
114
|
+
Bidirectional Mamba: runs forward and backward SSMs and combines outputs.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
d_model: Model dimension
|
|
118
|
+
d_state: State space dimension (default: 16)
|
|
119
|
+
d_conv: Local convolution width (default: 4)
|
|
120
|
+
expansion_factor: Inner dimension expansion (default: 2)
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
d_model: int,
|
|
126
|
+
d_state: int = 16,
|
|
127
|
+
d_conv: int = 4,
|
|
128
|
+
expansion_factor: int = 2,
|
|
129
|
+
):
|
|
130
|
+
super().__init__()
|
|
131
|
+
self.forward_block = MambaBlock(d_model, d_state, d_conv, expansion_factor)
|
|
132
|
+
self.backward_block = MambaBlock(d_model, d_state, d_conv, expansion_factor)
|
|
133
|
+
self.combine = nn.Linear(d_model * 2, d_model)
|
|
134
|
+
|
|
135
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
136
|
+
forward_out = self.forward_block(x)
|
|
137
|
+
backward_out = self.backward_block(x.flip(1)).flip(1)
|
|
138
|
+
return self.combine(torch.cat([forward_out, backward_out], dim=-1))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@register_model(
|
|
142
|
+
name="mamba",
|
|
143
|
+
description="Bidirectional Mamba state space model with linear complexity for ECG",
|
|
144
|
+
)
|
|
145
|
+
class Mamba1D(nn.Module):
|
|
146
|
+
"""
|
|
147
|
+
Bidirectional Mamba model for ECG classification.
|
|
148
|
+
|
|
149
|
+
Uses selective state space models with linear complexity as an
|
|
150
|
+
alternative to Transformer self-attention. The SSM is implemented
|
|
151
|
+
from scratch in pure PyTorch with no external dependencies.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
input_channels: Number of input channels (default: 1 for single-lead ECG)
|
|
155
|
+
output_size: Number of output classes
|
|
156
|
+
d_model: Model dimension (default: 128)
|
|
157
|
+
d_state: State space dimension (default: 16)
|
|
158
|
+
d_conv: Local convolution width (default: 4)
|
|
159
|
+
expansion_factor: Inner dimension expansion (default: 2)
|
|
160
|
+
num_layers: Number of bidirectional Mamba layers (default: 4)
|
|
161
|
+
patch_size: Patch size for input tokenization (default: 50)
|
|
162
|
+
dropout_rate: Dropout probability (default: 0.1)
|
|
163
|
+
max_patches: Maximum number of patches (default: 500)
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
>>> model = Mamba1D(input_channels=1, output_size=4)
|
|
167
|
+
>>> x = torch.randn(32, 1, 3000)
|
|
168
|
+
>>> output = model(x)
|
|
169
|
+
>>> print(output.shape)
|
|
170
|
+
|
|
171
|
+
>>> features = model.extract_features(x)
|
|
172
|
+
>>> print(features.shape) # (32, 128)
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def __init__(
|
|
176
|
+
self,
|
|
177
|
+
input_channels: int = 1,
|
|
178
|
+
output_size: int = 4,
|
|
179
|
+
d_model: int = 128,
|
|
180
|
+
d_state: int = 16,
|
|
181
|
+
d_conv: int = 4,
|
|
182
|
+
expansion_factor: int = 2,
|
|
183
|
+
num_layers: int = 4,
|
|
184
|
+
patch_size: int = 50,
|
|
185
|
+
dropout_rate: float = 0.1,
|
|
186
|
+
max_patches: int = 500,
|
|
187
|
+
):
|
|
188
|
+
super().__init__()
|
|
189
|
+
|
|
190
|
+
self.patch_embed = nn.Conv1d(
|
|
191
|
+
input_channels,
|
|
192
|
+
d_model,
|
|
193
|
+
kernel_size=patch_size,
|
|
194
|
+
stride=patch_size,
|
|
195
|
+
)
|
|
196
|
+
self.pos_embedding = nn.Parameter(torch.randn(1, max_patches, d_model) * 0.02)
|
|
197
|
+
|
|
198
|
+
self.layers = nn.ModuleList(
|
|
199
|
+
[
|
|
200
|
+
BidirectionalMamba(d_model, d_state, d_conv, expansion_factor)
|
|
201
|
+
for _ in range(num_layers)
|
|
202
|
+
]
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
self.norm = nn.LayerNorm(d_model)
|
|
206
|
+
self.dropout = nn.Dropout(dropout_rate)
|
|
207
|
+
self._feature_dim = d_model
|
|
208
|
+
|
|
209
|
+
self.classifier = nn.Linear(d_model, output_size)
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def feature_dim(self) -> int:
|
|
213
|
+
return self._feature_dim
|
|
214
|
+
|
|
215
|
+
def extract_features(self, x: torch.Tensor) -> torch.Tensor:
|
|
216
|
+
x = self.patch_embed(x)
|
|
217
|
+
x = x.transpose(1, 2)
|
|
218
|
+
num_patches = x.size(1)
|
|
219
|
+
x = x + self.pos_embedding[:, :num_patches]
|
|
220
|
+
|
|
221
|
+
for layer in self.layers:
|
|
222
|
+
x = layer(x)
|
|
223
|
+
|
|
224
|
+
x = self.norm(x)
|
|
225
|
+
x = x.mean(dim=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.classifier(x)
|
|
232
|
+
return x
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch import nn
|
|
3
|
+
|
|
4
|
+
from deepecgkit.registry import register_model
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PatchEmbedding1D(nn.Module):
|
|
8
|
+
"""
|
|
9
|
+
Converts 1D signal into patch embeddings at a given patch size.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
input_channels: Number of input ECG channels
|
|
13
|
+
d_model: Output embedding dimension
|
|
14
|
+
patch_size: Size of each non-overlapping patch
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, input_channels: int, d_model: int, patch_size: int):
|
|
18
|
+
super().__init__()
|
|
19
|
+
self.proj = nn.Conv1d(input_channels, d_model, kernel_size=patch_size, stride=patch_size)
|
|
20
|
+
|
|
21
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
22
|
+
x = self.proj(x)
|
|
23
|
+
return x.transpose(1, 2)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CrossChannelAttention(nn.Module):
|
|
27
|
+
"""
|
|
28
|
+
Cross-channel attention for multi-lead ECG signals.
|
|
29
|
+
|
|
30
|
+
For single-lead ECG this acts as an identity-like operation.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
d_model: Embedding dimension
|
|
34
|
+
nhead: Number of attention heads (default: 4)
|
|
35
|
+
dropout: Dropout probability (default: 0.1)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, d_model: int, nhead: int = 4, dropout: float = 0.1):
|
|
39
|
+
super().__init__()
|
|
40
|
+
self.attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=True)
|
|
41
|
+
self.norm = nn.LayerNorm(d_model)
|
|
42
|
+
|
|
43
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
44
|
+
residual = x
|
|
45
|
+
x, _ = self.attn(x, x, x)
|
|
46
|
+
return self.norm(x + residual)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class IntraGranularityBlock(nn.Module):
|
|
50
|
+
"""
|
|
51
|
+
Self-attention within patches of one granularity level.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
d_model: Embedding dimension
|
|
55
|
+
nhead: Number of attention heads (default: 8)
|
|
56
|
+
dim_feedforward: Feedforward dimension (default: 256)
|
|
57
|
+
dropout: Dropout probability (default: 0.1)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
d_model: int,
|
|
63
|
+
nhead: int = 8,
|
|
64
|
+
dim_feedforward: int = 256,
|
|
65
|
+
dropout: float = 0.1,
|
|
66
|
+
):
|
|
67
|
+
super().__init__()
|
|
68
|
+
encoder_layer = nn.TransformerEncoderLayer(
|
|
69
|
+
d_model=d_model,
|
|
70
|
+
nhead=nhead,
|
|
71
|
+
dim_feedforward=dim_feedforward,
|
|
72
|
+
dropout=dropout,
|
|
73
|
+
batch_first=True,
|
|
74
|
+
)
|
|
75
|
+
self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=1)
|
|
76
|
+
|
|
77
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
78
|
+
return self.encoder(x)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class InterGranularityAttention(nn.Module):
|
|
82
|
+
"""
|
|
83
|
+
Cross-attention to fuse information across different granularities.
|
|
84
|
+
|
|
85
|
+
Pools each granularity to a single vector, applies self-attention
|
|
86
|
+
across granularities, and returns updated representations.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
d_model: Embedding dimension
|
|
90
|
+
num_granularities: Number of granularity levels
|
|
91
|
+
nhead: Number of attention heads (default: 4)
|
|
92
|
+
dropout: Dropout probability (default: 0.1)
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
d_model: int,
|
|
98
|
+
num_granularities: int,
|
|
99
|
+
nhead: int = 4,
|
|
100
|
+
dropout: float = 0.1,
|
|
101
|
+
):
|
|
102
|
+
super().__init__()
|
|
103
|
+
self.attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=True)
|
|
104
|
+
self.norm = nn.LayerNorm(d_model)
|
|
105
|
+
self.num_granularities = num_granularities
|
|
106
|
+
|
|
107
|
+
def forward(self, granularity_features: list[torch.Tensor]) -> list[torch.Tensor]:
|
|
108
|
+
pooled = [f.mean(dim=1) for f in granularity_features]
|
|
109
|
+
stacked = torch.stack(pooled, dim=1)
|
|
110
|
+
attn_out, _ = self.attn(stacked, stacked, stacked)
|
|
111
|
+
attn_out = self.norm(attn_out + stacked)
|
|
112
|
+
|
|
113
|
+
updated = []
|
|
114
|
+
for i, feat in enumerate(granularity_features):
|
|
115
|
+
scale = attn_out[:, i].unsqueeze(1)
|
|
116
|
+
updated.append(feat + scale)
|
|
117
|
+
return updated
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@register_model(
|
|
121
|
+
name="medformer",
|
|
122
|
+
description="Multi-granularity patching Transformer for medical time series (NeurIPS 2024)",
|
|
123
|
+
)
|
|
124
|
+
class Medformer(nn.Module):
|
|
125
|
+
"""
|
|
126
|
+
Medformer: Multi-Granularity Patching Transformer for medical time series.
|
|
127
|
+
|
|
128
|
+
Uses multiple patch sizes to capture fine, medium, and coarse temporal
|
|
129
|
+
patterns, with intra-granularity self-attention and inter-granularity
|
|
130
|
+
cross-attention for information fusion.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
input_channels: Number of input channels (default: 1 for single-lead ECG)
|
|
134
|
+
output_size: Number of output classes
|
|
135
|
+
d_model: Transformer model dimension (default: 128)
|
|
136
|
+
patch_sizes: Tuple of patch sizes for different granularities (default: (10, 25, 50))
|
|
137
|
+
num_encoder_layers: Number of encoder layers (default: 2)
|
|
138
|
+
nhead: Number of attention heads (default: 8)
|
|
139
|
+
dim_feedforward: Feedforward dimension (default: 256)
|
|
140
|
+
dropout_rate: Dropout probability (default: 0.1)
|
|
141
|
+
max_patches: Maximum number of patches per granularity (default: 500)
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
>>> model = Medformer(input_channels=1, output_size=4)
|
|
145
|
+
>>> x = torch.randn(32, 1, 3000)
|
|
146
|
+
>>> output = model(x)
|
|
147
|
+
>>> print(output.shape)
|
|
148
|
+
|
|
149
|
+
>>> features = model.extract_features(x)
|
|
150
|
+
>>> print(features.shape) # (32, 384)
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
input_channels: int = 1,
|
|
156
|
+
output_size: int = 4,
|
|
157
|
+
d_model: int = 128,
|
|
158
|
+
patch_sizes: tuple[int, ...] = (10, 25, 50),
|
|
159
|
+
num_encoder_layers: int = 2,
|
|
160
|
+
nhead: int = 8,
|
|
161
|
+
dim_feedforward: int = 256,
|
|
162
|
+
dropout_rate: float = 0.1,
|
|
163
|
+
max_patches: int = 500,
|
|
164
|
+
):
|
|
165
|
+
super().__init__()
|
|
166
|
+
|
|
167
|
+
self.patch_sizes = patch_sizes
|
|
168
|
+
self.d_model = d_model
|
|
169
|
+
num_granularities = len(patch_sizes)
|
|
170
|
+
|
|
171
|
+
self.patch_embeddings = nn.ModuleList(
|
|
172
|
+
[PatchEmbedding1D(input_channels, d_model, ps) for ps in patch_sizes]
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
self.pos_embeddings = nn.ParameterList(
|
|
176
|
+
[nn.Parameter(torch.randn(1, max_patches, d_model) * 0.02) for _ in patch_sizes]
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
self.cross_channel_attn = CrossChannelAttention(
|
|
180
|
+
d_model,
|
|
181
|
+
nhead=min(4, nhead),
|
|
182
|
+
dropout=dropout_rate,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
self.intra_blocks = nn.ModuleList()
|
|
186
|
+
self.inter_blocks = nn.ModuleList()
|
|
187
|
+
for _ in range(num_encoder_layers):
|
|
188
|
+
self.intra_blocks.append(
|
|
189
|
+
nn.ModuleList(
|
|
190
|
+
[
|
|
191
|
+
IntraGranularityBlock(d_model, nhead, dim_feedforward, dropout_rate)
|
|
192
|
+
for _ in patch_sizes
|
|
193
|
+
]
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
self.inter_blocks.append(
|
|
197
|
+
InterGranularityAttention(d_model, num_granularities, min(4, nhead), dropout_rate)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
self.dropout = nn.Dropout(dropout_rate)
|
|
201
|
+
self._feature_dim = d_model * num_granularities
|
|
202
|
+
|
|
203
|
+
self.classifier = nn.Linear(self._feature_dim, output_size)
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def feature_dim(self) -> int:
|
|
207
|
+
return self._feature_dim
|
|
208
|
+
|
|
209
|
+
def extract_features(self, x: torch.Tensor) -> torch.Tensor:
|
|
210
|
+
granularity_features = []
|
|
211
|
+
for patch_emb, pos_emb in zip(self.patch_embeddings, self.pos_embeddings):
|
|
212
|
+
patches = patch_emb(x)
|
|
213
|
+
num_patches = patches.size(1)
|
|
214
|
+
patches = patches + pos_emb[:, :num_patches]
|
|
215
|
+
granularity_features.append(patches)
|
|
216
|
+
|
|
217
|
+
for intra_layer, inter_layer in zip(self.intra_blocks, self.inter_blocks):
|
|
218
|
+
for i, intra_block in enumerate(intra_layer):
|
|
219
|
+
granularity_features[i] = intra_block(granularity_features[i])
|
|
220
|
+
granularity_features = inter_layer(granularity_features)
|
|
221
|
+
|
|
222
|
+
pooled = [f.mean(dim=1) for f in granularity_features]
|
|
223
|
+
return torch.cat(pooled, dim=1)
|
|
224
|
+
|
|
225
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
226
|
+
x = self.extract_features(x)
|
|
227
|
+
x = self.dropout(x)
|
|
228
|
+
x = self.classifier(x)
|
|
229
|
+
return x
|
|
@@ -0,0 +1,189 @@
|
|
|
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 ResidualBlock1D(nn.Module):
|
|
9
|
+
"""
|
|
10
|
+
1D Residual block for ECG signal processing.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
in_channels: Number of input channels
|
|
14
|
+
out_channels: Number of output channels
|
|
15
|
+
kernel_size: Convolution kernel size (default: 7)
|
|
16
|
+
stride: Convolution stride (default: 1)
|
|
17
|
+
downsample: Downsample layer for skip connection if needed
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
in_channels: int,
|
|
23
|
+
out_channels: int,
|
|
24
|
+
kernel_size: int = 7,
|
|
25
|
+
stride: int = 1,
|
|
26
|
+
downsample=None,
|
|
27
|
+
):
|
|
28
|
+
super().__init__()
|
|
29
|
+
|
|
30
|
+
self.conv1 = nn.Conv1d(
|
|
31
|
+
in_channels,
|
|
32
|
+
out_channels,
|
|
33
|
+
kernel_size=kernel_size,
|
|
34
|
+
stride=stride,
|
|
35
|
+
padding=kernel_size // 2,
|
|
36
|
+
bias=False,
|
|
37
|
+
)
|
|
38
|
+
self.bn1 = nn.BatchNorm1d(out_channels)
|
|
39
|
+
|
|
40
|
+
self.conv2 = nn.Conv1d(
|
|
41
|
+
out_channels,
|
|
42
|
+
out_channels,
|
|
43
|
+
kernel_size=kernel_size,
|
|
44
|
+
stride=1,
|
|
45
|
+
padding=kernel_size // 2,
|
|
46
|
+
bias=False,
|
|
47
|
+
)
|
|
48
|
+
self.bn2 = nn.BatchNorm1d(out_channels)
|
|
49
|
+
|
|
50
|
+
self.downsample = downsample
|
|
51
|
+
|
|
52
|
+
def forward(self, x):
|
|
53
|
+
identity = x
|
|
54
|
+
|
|
55
|
+
out = self.conv1(x)
|
|
56
|
+
out = self.bn1(out)
|
|
57
|
+
out = func.relu(out)
|
|
58
|
+
|
|
59
|
+
out = self.conv2(out)
|
|
60
|
+
out = self.bn2(out)
|
|
61
|
+
|
|
62
|
+
if self.downsample is not None:
|
|
63
|
+
identity = self.downsample(x)
|
|
64
|
+
|
|
65
|
+
out += identity
|
|
66
|
+
out = func.relu(out)
|
|
67
|
+
|
|
68
|
+
return out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@register_model(
|
|
72
|
+
name="resnet",
|
|
73
|
+
description="1D ResNet architecture adapted for ECG",
|
|
74
|
+
)
|
|
75
|
+
class ResNet1D(nn.Module):
|
|
76
|
+
"""
|
|
77
|
+
1D ResNet model for ECG signal classification.
|
|
78
|
+
|
|
79
|
+
A residual network architecture adapted for 1D time-series ECG data,
|
|
80
|
+
providing deep feature extraction with skip connections.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
input_channels: Number of input channels (default: 1 for single-lead ECG)
|
|
84
|
+
output_size: Number of output classes
|
|
85
|
+
base_channels: Base number of channels (default: 64)
|
|
86
|
+
num_blocks: List of number of blocks in each layer (default: [2, 2, 2, 2])
|
|
87
|
+
dropout_rate: Dropout probability (default: 0.3)
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
>>> model = ResNet1D(input_channels=1, output_size=4)
|
|
91
|
+
>>> x = torch.randn(32, 1, 3000)
|
|
92
|
+
>>> output = model(x)
|
|
93
|
+
>>> print(output.shape)
|
|
94
|
+
|
|
95
|
+
>>> features = model.extract_features(x)
|
|
96
|
+
>>> print(features.shape) # (32, 512) with base_channels=64
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
input_channels: int = 1,
|
|
102
|
+
output_size: int = 4,
|
|
103
|
+
base_channels: int = 64,
|
|
104
|
+
num_blocks: list | None = None,
|
|
105
|
+
dropout_rate: float = 0.3,
|
|
106
|
+
):
|
|
107
|
+
super().__init__()
|
|
108
|
+
|
|
109
|
+
if num_blocks is None:
|
|
110
|
+
num_blocks = [2, 2, 2, 2]
|
|
111
|
+
|
|
112
|
+
self.in_channels = base_channels
|
|
113
|
+
|
|
114
|
+
self.conv1 = nn.Conv1d(
|
|
115
|
+
input_channels,
|
|
116
|
+
base_channels,
|
|
117
|
+
kernel_size=15,
|
|
118
|
+
stride=2,
|
|
119
|
+
padding=7,
|
|
120
|
+
bias=False,
|
|
121
|
+
)
|
|
122
|
+
self.bn1 = nn.BatchNorm1d(base_channels)
|
|
123
|
+
self.relu = nn.ReLU(inplace=True)
|
|
124
|
+
self.maxpool = nn.MaxPool1d(kernel_size=3, stride=2, padding=1)
|
|
125
|
+
|
|
126
|
+
self.layer1 = self._make_layer(base_channels, num_blocks[0], stride=1)
|
|
127
|
+
self.layer2 = self._make_layer(base_channels * 2, num_blocks[1], stride=2)
|
|
128
|
+
self.layer3 = self._make_layer(base_channels * 4, num_blocks[2], stride=2)
|
|
129
|
+
self.layer4 = self._make_layer(base_channels * 8, num_blocks[3], stride=2)
|
|
130
|
+
|
|
131
|
+
self.avgpool = nn.AdaptiveAvgPool1d(1)
|
|
132
|
+
self.dropout = nn.Dropout(dropout_rate)
|
|
133
|
+
self._feature_dim = base_channels * 8
|
|
134
|
+
self.fc = nn.Linear(self._feature_dim, output_size)
|
|
135
|
+
|
|
136
|
+
def _make_layer(self, out_channels: int, num_blocks: int, stride: int = 1):
|
|
137
|
+
downsample = None
|
|
138
|
+
if stride != 1 or self.in_channels != out_channels:
|
|
139
|
+
downsample = nn.Sequential(
|
|
140
|
+
nn.Conv1d(
|
|
141
|
+
self.in_channels,
|
|
142
|
+
out_channels,
|
|
143
|
+
kernel_size=1,
|
|
144
|
+
stride=stride,
|
|
145
|
+
bias=False,
|
|
146
|
+
),
|
|
147
|
+
nn.BatchNorm1d(out_channels),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
layers = []
|
|
151
|
+
layers.append(
|
|
152
|
+
ResidualBlock1D(
|
|
153
|
+
self.in_channels,
|
|
154
|
+
out_channels,
|
|
155
|
+
stride=stride,
|
|
156
|
+
downsample=downsample,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
self.in_channels = out_channels
|
|
160
|
+
|
|
161
|
+
for _ in range(1, num_blocks):
|
|
162
|
+
layers.append(ResidualBlock1D(out_channels, out_channels))
|
|
163
|
+
|
|
164
|
+
return nn.Sequential(*layers)
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def feature_dim(self) -> int:
|
|
168
|
+
return self._feature_dim
|
|
169
|
+
|
|
170
|
+
def extract_features(self, x: torch.Tensor) -> torch.Tensor:
|
|
171
|
+
x = self.conv1(x)
|
|
172
|
+
x = self.bn1(x)
|
|
173
|
+
x = self.relu(x)
|
|
174
|
+
x = self.maxpool(x)
|
|
175
|
+
|
|
176
|
+
x = self.layer1(x)
|
|
177
|
+
x = self.layer2(x)
|
|
178
|
+
x = self.layer3(x)
|
|
179
|
+
x = self.layer4(x)
|
|
180
|
+
|
|
181
|
+
x = self.avgpool(x)
|
|
182
|
+
x = torch.flatten(x, 1)
|
|
183
|
+
return x
|
|
184
|
+
|
|
185
|
+
def forward(self, x):
|
|
186
|
+
x = self.extract_features(x)
|
|
187
|
+
x = self.dropout(x)
|
|
188
|
+
x = self.fc(x)
|
|
189
|
+
return x
|