Diffusion (DDPM) — Visual Guide¶
A diffusion model learns to undo noise. In training, a clean ECG is mixed with Gaussian noise at a
random level t and the network learns to predict what was added (here: the velocity v). At
sampling time it starts from pure noise and removes it in about 50 DDIM steps.
Code: src/ecgen/families/continuous/module.py (DiffusionModule), process
src/ecgen/processes/ddpm.py, guidance src/ecgen/processes/base.py, backbone
src/ecgen/nn/backbones/unet1d.py, conditioning src/ecgen/conditioning/composite.py, EMA
src/ecgen/training/ema.py. Written reference: Diffusion and flow matching.
One training step¶
The forward (noising) process¶
DDPMProcess precomputes alphas_cumprod ᾱt for num_steps = 1000. Drag t to
see how much of the ECG survives. The plot shows ᾱt for the default cosine
schedule and the linear one, plus the min-SNR loss weight for v-prediction (γ = 5).
_schedule(); the ECG is schematic. The linear schedule destroys the signal much earlier, so many of its 1000 steps are spent on near-pure noise.Architecture: UNet1d¶
The default backbone is an ADM-style 1-D U-Net with base_channels = 64,
channel_mults = [1, 2, 4, 4], num_res_blocks = 2 and attention at level 3. The three
stride-2 convolutions take 5000 → 2500 → 1250 → 625, so the length must be divisible by 8.
Each ResBlock: where time and condition enter¶
Conditioning: three slots¶
CompositeConditioner turns each condition field into one of three slots. During training each
field is replaced by a learned null embedding with its own probability (conditioning.dropout),
so the same network also learns the unconditional model needed for guidance.
ssm backbone accepts vector and dense slots only; unet1d and dit1d accept all three.Other backbones¶
The process and the conditioning do not care which network predicts v. Two other backbones
take the same forward(x, t, cond):
dit1d(src/ecgen/nn/backbones/dit1d.py): cuts the signal into patches of 25 samples (5000 → 200 tokens of width 256), runs 8 transformer blocks with AdaLN-Zero modulation from the time + condition embedding and cross-attention to condition tokens in every block, then un-patches with a zero-initialised Linear.ssm(src/ecgen/nn/backbones/ssm.py): SSSD-style stack of 8 residual blocks with bidirectional S4D layers at full resolution (128 channels, no downsampling; the receptive field is the whole signal).mambaswaps S4D for bidirectional Mamba.
Classifier-free guidance¶
With a conditional model and guidance = w ≠ 1, every sampling step runs the backbone twice: once
with the condition (c) and once with all fields replaced by their nulls (u,
encode_condition(..., drop="all")). cfg_model_fn combines them as u + w·(c − u).
guidance = 1.0 (off).How the loss is computed¶
# ContinuousGenerativeModule._loss (simplified: no masks)
x0 = self.to_model_space(signal) # identity for DiffusionModule
cond = self.adapt_condition(self.condition(batch), x0.shape[-1]) # with per-field dropout
pair = self.process.training_pair(x0, torch.randn_like(x0)) # x_t, t, target, weight
out = self.backbone(pair.xt, pair.t, cond)
per_sample = ((out - pair.target) ** 2).flatten(1).mean(1)
loss = (pair.weight * per_sample).mean()
With the default prediction = "v" and min_snr_gamma = 5:
Partial-ECG options
With data.masks, loss_on: unobserved averages the error only where obs_mask = 0, and
clean_observed: true (SSSD-style) overwrites the observed entries of xt with the
clean signal before the backbone, in training and at every sampling step.
Sampling¶
sample() swaps in the EMA weights, encodes the condition (and its null version for guidance),
then lets DDPMProcess.sample run DDIM from pure noise over steps evenly spaced time steps
(default 50 of the 1000).
The widget runs that loop on one lead. Change the number of steps, η, and the inpainting mode:
replace overwrites the observed part (here the first 35 %) with the observation noised to the
next level after every step; RePaint (inpaint="repaint", jumps=5) additionally diffuses
back from t′ to t and redoes the step, 5 times per step, so the generated part can adjust to the
observed part.
| Option | Where | Effect |
|---|---|---|
steps |
sample(steps=), default sample_steps = 50 |
network calls (× 2 with guidance, × jumps with RePaint) |
eta |
sample(eta=), default 0 |
0 = deterministic DDIM, 1 = ancestral |
guidance |
sample(guidance=), default 1.0 |
classifier-free guidance scale w |
inpaint |
"replace", "repaint"; "clean" is automatic for clean_observed models |
training-free partial-ECG completion |
use_ema |
default True | sample with the EMA weights |
Flow matching uses the same module, backbone and conditioning with a different process; see Flow matching.