ForeverBlue commited on
Commit
aa31537
·
verified ·
1 Parent(s): 98c067c

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ tags:
4
+ - ecg
5
+ - electrocardiogram
6
+ - biosignal
7
+ - self-supervised
8
+ - masked-autoencoder
9
+ - pretrained
10
+ library_name: pytorch
11
+ ---
12
+
13
+ # ECG C1 multi-route pretrained encoder
14
+
15
+ Masked dual-reconstruction pretraining on 12-lead and single-lead ECG. One shared transformer serves two lead configurations through route-specific wavelet frontends, and lead identity enters as a learned embedding of the lead NAME rather than of its index.
16
+
17
+ Trained for 57 epochs of 60 configured
18
+ (57000 optimizer updates) on a mixture of public
19
+ ECG corpora, 30.1 M parameters. The files
20
+ here are built around `pretrain_ecg_c1_moe_best.pth`, epoch 33.
21
+
22
+ ## Objective
23
+
24
+ ```
25
+ 0.5*L_spec + 0.5*L_raw + 0.001*L_foldKL
26
+ ```
27
+
28
+ Two heads at equal weight. One predicts folded wavelet patches, the other
29
+ predicts the preprocessed ECG waveform of the same masked patches.
30
+ Masking is applied BEFORE the wavelet frontend (True),
31
+ at ratio 0.7, and the spec target is per-patch
32
+ normalised (True).
33
+
34
+ ## Validation, at the released checkpoint (epoch 33)
35
+
36
+ | metric | value | what 1.0 would mean |
37
+ |---|---|---|
38
+ | total loss | 0.1827 | — |
39
+ | spec MSE | 0.2562 | — |
40
+ | spec NMSE | 0.256 | predicting zero |
41
+ | spec Pearson r | 0.851 | perfect |
42
+ | raw SmoothL1 | 0.1092 | — |
43
+ | raw NMSE | 0.255 | predicting zero |
44
+ | raw Pearson r | 0.845 | perfect |
45
+ | route-macro total | 0.2041 | — |
46
+
47
+ NMSE is dimensionless: MSE(prediction, target) / MSE(0, target), so 1.0 is
48
+ what predicting zero scores on that head's own target. The two losses are NOT
49
+ comparable to each other -- one is an MSE on normalised wavelet coefficients,
50
+ the other a SmoothL1 on the window-normalised waveform. The columns that compare the
51
+ heads are the correlations and the NMSEs.
52
+
53
+ The route-macro row weights the two routes equally. The plain rows weight by
54
+ sample count, and validation is dominated by the largest corpora, so a model
55
+ specialising toward the smaller ones moves the two rows in opposite
56
+ directions.
57
+
58
+ ## Files
59
+
60
+ ### Which file do you want
61
+
62
+ | your situation | download |
63
+ |---|---|
64
+ | fine-tune on any montage | `ecg_c1_encoder.pth` |
65
+ | your montage IS one of the two routes below | `pretrain_ecg_c1_moe_best.pth`, then cut the route out (one command, below) |
66
+ | continue pretraining, or take the model apart yourself | `pretrain_ecg_c1_moe_best.pth` |
67
+
68
+ **`pretrain_ecg_c1_moe_best.pth` is the complete model** -- all two wavelet frontends, the
69
+ patchers, both reconstruction decoders. Nothing is held back; only the
70
+ optimiser state was stripped, which no downstream use needs. Take it if you
71
+ want to keep or strip pieces yourself.
72
+
73
+ `ecg_c1_encoder.pth` is the same weights minus everything route-bound and
74
+ pretraining-only. It exists because it cannot be loaded wrong.
75
+
76
+ | file | what it is |
77
+ |---|---|
78
+ | `ecg_c1_encoder.pth` | encoder alone: channel embedding, gate, transformer |
79
+ | `pretrain_ecg_c1_moe_best.pth` | complete model, lowest validation **total** loss (epoch 33) |
80
+ | `pretrain_ecg_c1_moe_final.pth` | complete model, last epoch (epoch 57) |
81
+ | `pretrain_ecg_c1_moe_best_spec.pth` | complete model, lowest spec MSE (epoch 19) |
82
+ | `pretrain_ecg_c1_moe_best_raw.pth` | complete model, lowest raw SmoothL1 (epoch 37) |
83
+ | `pretrain_ecg_c1_moe_best_macro.pth` | complete model, lowest route-macro total (epoch 28) |
84
+ | `metrics_epoch.jsonl` | the full training curve |
85
+ | `config_resolved.yaml` | every resolved hyperparameter |
86
+
87
+ ### Why several checkpoints
88
+
89
+ Each is the minimum of a different validation metric, and they are published
90
+ together so a downstream comparison can ask which criterion transfers instead
91
+ of assuming the one that names `best.pth`. Reconstruction loss cannot answer
92
+ that question; a downstream task can.
93
+
94
+ `best.pth` is selected on the TOTAL loss -- the loss that was trained (epoch
95
+ 32 counted from 0). It is not the best spec loss, and not
96
+ the best raw loss.
97
+
98
+ ### What is in the complete model
99
+
100
+ | component | parameters | share | |
101
+ |---|---|---|---|
102
+ | shared transformer | 28.38 M | 94.3% | encoder, always transfers |
103
+ | channel-name embedding | 0.00 M | 0.0% | always transfers |
104
+ | channel projection | 0.03 M | 0.1% | always transfers |
105
+ | wavelet frontends (all two routes) | 0.03 M | 0.1% | route-bound; downstream usually builds its own |
106
+ | patchers (both rates) | 0.20 M | 0.6% | route-bound |
107
+ | spec decoder | 0.72 M | 2.4% | pretraining only |
108
+ | raw decoder | 0.72 M | 2.4% | pretraining only |
109
+ | **total** | **30.08 M** | 100% | |
110
+
111
+ The shared transformer is 94% of the model and the two frontends together are 0%, which is why there is no per-route encoder file here: two of them would duplicate that transformer two times to deliver frontends that are a small fraction of it. Cut the one you want out of the complete checkpoint instead.
112
+
113
+ ### What transfers, and what does not
114
+
115
+ `downstream.py` splits a checkpoint into two sets. The channel embedding, its
116
+ gate and the shared transformer **always** load. The wavelet frontend and the
117
+ patcher load **only when your montage is the route they were trained for** --
118
+ same channel count, same patch length -- and are skipped otherwise rather
119
+ than reshaped.
120
+
121
+ A 12-lead recording at 500 Hz in 10 s windows IS the `L12_500` route, so on the most common ECG input the pretrained frontend and patcher transfer as well: cut that route out (below) and pass `route_id="L12_500"`. Any other lead set, rate or window length builds its own.
122
+
123
+ The two routes the shared transformer was pretrained across:
124
+
125
+ | route | channels | rate |
126
+ |---|---|---|
127
+ | `L12_500` | 12 | 500 Hz |
128
+ | `L1_250` | 1 | 250 Hz |
129
+
130
+ To fine-tune ON one of them exactly, take that route's frontend and patcher out
131
+ of the complete checkpoint:
132
+
133
+ ```bash
134
+ python scripts/export_eeg_pretrained_encoder.py \
135
+ --checkpoint pretrain_ecg_c1_moe_best.pth --route L12_500 \
136
+ --output encoder_L12_500.pth
137
+ ```
138
+
139
+ The exporter reads the modality from the checkpoint; its name is historical.
140
+ Omit `--route` and you get `ecg_c1_encoder.pth` again. Both pretraining decoders are
141
+ dropped either way: fine-tuning against the pretext objective by accident is
142
+ the failure that prevents.
143
+
144
+ The released checkpoint and the last epoch differ by 2.6% in validation total loss (0.1827 against 0.1877). Which transfers better is a downstream question, not a reconstruction-loss question -- measure both.
145
+
146
+ ## The channel vocabulary travels with the weights
147
+
148
+ An embedding row means whatever channel held that id when the row was learned.
149
+ Every checkpoint here carries the vocabulary and its hash. Resolving
150
+ lead names against a different vocabulary silently trains on relabelled
151
+ channels, which is why the hash is checked rather than assumed.
152
+
153
+ ## Architecture
154
+
155
+ ```
156
+ embed_dim 512 depth 9 heads 8
157
+ norm rmsnorm ffn swiglu qk_norm True
158
+ wavelet max_level 3, kernel 16, bank db4, db6, sym4, sym8, coif2
159
+ channel id embedding, injected at the token site
160
+ mask frequency_guided, ratio 0.7
161
+ ```
162
+
163
+ ## Loading
164
+
165
+ `ECGC1Downstream` builds the frontend and patcher for your montage and loads this
166
+ file into everything else. Nothing here is route-bound, so the montage, the
167
+ window length and the patch length are yours to choose.
168
+
169
+ ```python
170
+ import torch
171
+ from physiowave.ecg_c1.downstream import ECGC1Downstream
172
+
173
+ ENC = "ecg_c1_encoder.pth"
174
+ ck = torch.load(ENC, map_location="cpu", weights_only=False)
175
+ mc = ck["model_config"] # the width the weights were trained at
176
+ CH = ["I", "II", "III", "aVR", "aVL", "aVF",
177
+ "V1", "V2", "V3", "V4", "V5", "V6"] # your leads
178
+
179
+ model = ECGC1Downstream(
180
+ in_channels=len(CH), window_samples=5000, sampling_rate=500.0,
181
+ patch_samples=250, num_classes=2, channel_names=CH,
182
+ embed_dim=mc["embed_dim"], depth=mc["depth"], num_heads=mc["num_heads"],
183
+ channel_embed_dim=mc.get("channel_embed_dim", 64),
184
+ channel_vocab_size=ck["channel_vocab_size"])
185
+
186
+ report = model.load_pretrained(ENC)
187
+ print(model.describe_transfer(report))
188
+ # loaded: transformer N, channel encoder N, gate yes, frontend 0, patcher 0
189
+
190
+ class Meta: # or your dataset's ChannelMeta
191
+ channel_names = CH
192
+ channel_mask = None
193
+
194
+ out = model(torch.randn(2, len(CH), 5000), Meta()) # {"logits": [2, 2]}
195
+ ```
196
+
197
+ `frontend 0, patcher 0` is the expected line, not a warning: those are the two
198
+ things your montage supplies.
199
+
200
+ **The frontend you build is not randomly initialised.** Its filters are seeded
201
+ from real wavelet families -- `db4, db6, sym4, sym8, coif2` -- and learned from there during
202
+ fine-tuning. What a fresh frontend costs you is the adaptation those filters
203
+ underwent during pretraining, not the wavelet prior itself.
204
+
205
+ Names, not indices, are what this encoder and your data agree on. Pass real
206
+ lead names and check `channel_vocab_sha256` -- `load_pretrained` refuses a
207
+ mismatch rather than silently training on relabelled channels.
208
+
209
+ Code: <https://github.com/ForeverBlue816/PhysioWave-T>
210
+
211
+ ---
212
+ Prepared by `scripts/prepare_hf_release.py` from `pretrain_ecg_c1_moe`
213
+ on 2026-10-29.
config_resolved.yaml ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ data:
2
+ manifest_train: <path redacted for release>
3
+ manifest_val: <path redacted for release>
4
+ modality: ecg
5
+ weights: temperature:0.5
6
+ model:
7
+ channel_embed_dim: 64
8
+ channel_encoding: id
9
+ channel_injection: token
10
+ channel_token_gate_init: 0.0
11
+ depth: 9
12
+ dropout: 0.0
13
+ embed_dim: 512
14
+ ffn: swiglu
15
+ fold_gamma: 0.1
16
+ fold_synthesis: 3
17
+ importance_ratio: 0.6
18
+ lead_group_masking: true
19
+ mask_ratio: 0.7
20
+ masking_strategy: frequency_guided
21
+ max_level: 3
22
+ mlp_ratio: 4.0
23
+ norm: rmsnorm
24
+ num_heads: 8
25
+ qk_norm: true
26
+ scale_fold: dynamic
27
+ use_separate_channel: true
28
+ wave_init_mode: pad
29
+ wave_kernel_size: 16
30
+ wavelet_names:
31
+ - db4
32
+ - db6
33
+ - sym4
34
+ - sym8
35
+ - coif2
36
+ objective:
37
+ fold_kl: 0.001
38
+ mask_before_frontend: true
39
+ normalize_spec_target: true
40
+ raw_beta: 0.5
41
+ raw_weight: 0.5
42
+ spec_weight: 0.5
43
+ output_dir: <path redacted for release>
44
+ run_name: pretrain_ecg_c1_moe
45
+ seed: 42
46
+ train:
47
+ batch_size_by_route:
48
+ L12_500: 96
49
+ L1_250: 1024
50
+ clip_grad_norm: 1.0
51
+ epochs: 60
52
+ grad_accumulation_steps: 1
53
+ lr: 0.0003
54
+ min_lr_ratio: 0.01
55
+ precision: bf16
56
+ steps_per_epoch: 1000
57
+ val_mask_seed: 1234
58
+ val_max_batches_per_dataset: 100
59
+ warmup_epochs: 2
60
+ weight_decay: 0.05
61
+ trainer: ecg_c1_moe
ecg_c1_encoder.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6f8b22f458c2de48520e228d41a094c0498ac77850aa3ec450a901fca2a8edad
3
+ size 113689120
metrics_epoch.jsonl ADDED
The diff for this file is too large to render. See raw diff
 
pretrain_ecg_c1_moe_best.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8a8431de5d302277f9985e59b1c2c6c11bde73715279672cd6712ff127c3c408
3
+ size 120442759
pretrain_ecg_c1_moe_best_macro.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9c758845d0367f170b825f9757c54045f08ee09f86ae035ece60fd9197040980
3
+ size 120444633
pretrain_ecg_c1_moe_best_raw.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2e3dc6163d1a64fca29ccc1e7393b56165ddc7be5c915c1e9267525c3ac575a5
3
+ size 120443923
pretrain_ecg_c1_moe_best_spec.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:86a5d10270f20abf7ead05ab297b05cf10fefed8cc012b2c63a74d8189398849
3
+ size 120444214
pretrain_ecg_c1_moe_final.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5236c2f7a0f7db3d9fe1316e52f10198416f8f3b4738035fe3f5657ee8f78587
3
+ size 120443050