deepecgkit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. deepecgkit/__init__.py +42 -0
  2. deepecgkit/__main__.py +6 -0
  3. deepecgkit/cli/__init__.py +61 -0
  4. deepecgkit/cli/commands/__init__.py +16 -0
  5. deepecgkit/cli/commands/evaluate.py +83 -0
  6. deepecgkit/cli/commands/info.py +73 -0
  7. deepecgkit/cli/commands/predict.py +94 -0
  8. deepecgkit/cli/commands/train.py +331 -0
  9. deepecgkit/cli/config.py +23 -0
  10. deepecgkit/cli/evaluation.py +251 -0
  11. deepecgkit/cli/logger.py +26 -0
  12. deepecgkit/cli/main.py +429 -0
  13. deepecgkit/cli/registry.py +32 -0
  14. deepecgkit/datasets/__init__.py +33 -0
  15. deepecgkit/datasets/af_classification.py +301 -0
  16. deepecgkit/datasets/base.py +146 -0
  17. deepecgkit/datasets/ltafdb.py +363 -0
  18. deepecgkit/datasets/mitbih_afdb.py +287 -0
  19. deepecgkit/datasets/modules.py +222 -0
  20. deepecgkit/datasets/preprocessing.py +208 -0
  21. deepecgkit/datasets/ptbxl.py +567 -0
  22. deepecgkit/datasets/splitting.py +225 -0
  23. deepecgkit/datasets/unified_af.py +316 -0
  24. deepecgkit/evaluation/__init__.py +40 -0
  25. deepecgkit/evaluation/evaluator.py +398 -0
  26. deepecgkit/evaluation/metrics.py +164 -0
  27. deepecgkit/evaluation/visualization.py +285 -0
  28. deepecgkit/models/__init__.py +51 -0
  29. deepecgkit/models/af_classifier.py +177 -0
  30. deepecgkit/models/convnext_v2_1d.py +183 -0
  31. deepecgkit/models/crnn.py +117 -0
  32. deepecgkit/models/deep_res_cnn.py +215 -0
  33. deepecgkit/models/ecg_dualnet.py +230 -0
  34. deepecgkit/models/fcn_wang.py +88 -0
  35. deepecgkit/models/gru_model.py +99 -0
  36. deepecgkit/models/inception_time.py +197 -0
  37. deepecgkit/models/kanres_wide_x.py +150 -0
  38. deepecgkit/models/kanres_x.py +126 -0
  39. deepecgkit/models/lstm_model.py +96 -0
  40. deepecgkit/models/mamba1d.py +232 -0
  41. deepecgkit/models/medformer.py +229 -0
  42. deepecgkit/models/resnet1d.py +189 -0
  43. deepecgkit/models/resnet1d_wang.py +182 -0
  44. deepecgkit/models/se_resnet1d.py +232 -0
  45. deepecgkit/models/simple_cnn.py +82 -0
  46. deepecgkit/models/tcn.py +178 -0
  47. deepecgkit/models/transformer_ecg.py +127 -0
  48. deepecgkit/models/xresnet1d.py +265 -0
  49. deepecgkit/models/xresnet1d_benchmark.py +227 -0
  50. deepecgkit/registry.py +97 -0
  51. deepecgkit/training/__init__.py +9 -0
  52. deepecgkit/training/train.py +171 -0
  53. deepecgkit/utils/__init__.py +72 -0
  54. deepecgkit/utils/download.py +123 -0
  55. deepecgkit/utils/weights.py +107 -0
  56. deepecgkit-0.1.0.dist-info/METADATA +99 -0
  57. deepecgkit-0.1.0.dist-info/RECORD +61 -0
  58. deepecgkit-0.1.0.dist-info/WHEEL +5 -0
  59. deepecgkit-0.1.0.dist-info/entry_points.txt +2 -0
  60. deepecgkit-0.1.0.dist-info/licenses/LICENSE +201 -0
  61. deepecgkit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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