Visualization¶
ECGEN has two visualization pieces:
- During training —
VAEVisualizationCallback(insrc/ecgen/training/callbacks/visualization.py) saves real-vs-reconstruction plots for the VAE and VQ-VAE. - Offline — the
visualizationpackage (visualization/ecg_plots.py,latent_space.py,training_curves.py) with plotting helpers that return matplotlib figures.
GAN-specific callbacks (ECGVisualizationCallback, GeneratedSamplesCallback) are described in
Callbacks.
Reconstruction plots during training¶
vae_mimic.yaml and vqvae_mimic.yaml list a vae_visualization callback in callbacks:; its
params are the VAEVisualizationCallback arguments:
callbacks:
- name: vae_visualization
params:
log_every_n_epochs: 5 # plot every N epochs (only on epochs where validation runs)
num_samples: 4 # ECGs per plot, taken from the first validation batch
plot_all_leads: true # true: one subplot per lead; false: all leads overlaid
log_to_tensorboard: false
log_to_wandb: true # also needs wandb.enabled: true
# save_dir defaults to ${run_dir}/samples
Change a value from the command line with e.g. callbacks.2.params.log_every_n_epochs=1 (the index
is the entry's position in the list). The prior config (prior_mimic.yaml) has no visualization.
The old top-level visualization: section is no longer accepted.
Plots appear only on validation epochs
The callback runs in on_validation_epoch_end, skips the sanity check, and fires when
current_epoch % log_every_n_epochs == 0 (0-based epochs). Lightning validates when
(current_epoch + 1) % check_val_every_n_epoch == 0, i.e. on epochs 4, 9, 14, ... for
check_val_every_n_epoch: 5. A plot is produced only on epochs that satisfy both conditions —
in particular, the shipped
vae_mimic.yaml (check_val_every_n_epoch: 5, log_every_n_epochs: 5) never produces a
plot. Use log_every_n_epochs: 1 (as vqvae_mimic.yaml does) to plot on every validation epoch.
Plot layouts¶
plot_all_leads: true — a grid with 2 × num_samples rows and one column per lead: for each
sample a "Real" row (blue) above a "Recon" row (orange). Columns are titled with the fixed lead
names I, II, III, aVR, aVL, aVF, V1–V6.
Lead titles are fixed
The titles do not come from the batch's lead_names. MIMIC-IV-ECG stores aVF before aVL, so
with the default leads: null the aVL/aVF titles are swapped, and with any other lead selection
(e.g. independent8) the titles do not match the channels.
plot_all_leads: false — num_samples rows × 2 columns ("Real" and "Reconstructed"), with all
leads overlaid in each panel and a lead legend on the first row.
Both figures are titled VAE Reconstruction - Epoch N (also for the VQ-VAE).
Where the plots go¶
| Destination | Location |
|---|---|
| Disk | runs/<exp_name>/seed_<seed>/samples/epoch_{epoch:04d}.png (150 dpi) |
| TensorBoard | Images tab, tag vae_reconstruction (when log_to_tensorboard is on) |
| Weights & Biases | key vae_reconstruction (when log_to_wandb is on and a WandbLogger exists) |
Using the callback directly¶
import pytorch_lightning as pl
from ecgen.training.callbacks import VAEVisualizationCallback
viz = VAEVisualizationCallback(
save_dir="runs/my_exp/seed_42/samples",
log_every_n_epochs=1,
num_samples=8,
plot_all_leads=True,
log_to_tensorboard=True,
log_to_wandb=False,
)
trainer = pl.Trainer(callbacks=[viz], max_epochs=10)
Requirements on the model and data:
- validation batches must contain an ECG tensor readable by
ecgen.types.get_signal(anECGBatchfromECGDataModule, a{"signal"}/ legacy{"ecg_signals"}dict, or an(ecg, features)tuple); - the LightningModule must expose
vaeorvqvae(or be callable itself) returning a tuple whose first element is the reconstruction.
Reading the plots¶
| Observation | Likely cause | Try |
|---|---|---|
| Smooth / blurry reconstructions, missing P or T waves | KL term too strong (VAE) or too little capacity | Lower kl_weight; raise base_channels / num_res_blocks |
| Reconstructions do not improve across epochs | Learning-rate problem | Adjust lr |
Excellent reconstructions but poor sample() output (VAE) |
Latent not close to N(0, I) | Raise kl_weight |
Troubleshooting¶
- No images: validation runs on 0-based epochs
ewith(e + 1) % check_val_every_n_epoch == 0, and the callback needse % log_every_n_epochs == 0on such an epoch (see the warning above). Settinglog_every_n_epochs: 1always works. Images are also skipped during the sanity check. - Nothing in TensorBoard:
configs/experiments/vae_mimic.yamlandvqvae_mimic.yamlship withlog_to_tensorboard: false; set it totrue(e.g.callbacks.2.params.log_to_tensorboard=true). - Large figures / memory: lower
num_samplesor useplot_all_leads: false.
The visualization package¶
from visualization import (
plot_ecg_leads, plot_ecg_comparison, plot_ecg_overlay,
plot_latent_space_2d, plot_latent_distribution,
plot_training_curves, plot_multiple_metrics, plot_learning_rate_schedule,
)
Every function returns a matplotlib.figure.Figure; save it with fig.savefig(...). ECG inputs
can be torch.Tensor or numpy.ndarray; time axes are in seconds using sampling_rate (default
500 Hz).
ECG plots — visualization/ecg_plots.py¶
| Function | Signature (defaults) | Plots |
|---|---|---|
plot_ecg_leads |
(ecg, lead_names=None, sampling_rate=500, title="ECG Signal", figsize=(15, 10)) |
All leads of one ECG (num_leads, seq_length), one row per lead |
plot_ecg_comparison |
(real, generated, lead_idx=0, lead_name="Lead I", sampling_rate=500, figsize=(12, 4)) |
One lead, real above generated |
plot_ecg_overlay |
(signals, labels, lead_idx=0, lead_name="Lead I", sampling_rate=500, figsize=(12, 4)) |
One lead from several signals, overlaid |
plot_ecg_leads needs at least two leads (it indexes the subplot array).
Latent space — visualization/latent_space.py¶
| Function | Signature (defaults) | Plots |
|---|---|---|
plot_latent_space_2d |
(latents, labels=None, method="pca", title="Latent Space Visualization", figsize=(10, 8)) |
2-D PCA ("pca") or t-SNE ("tsne") projection; 3-D latents (N, C, L) are flattened |
plot_latent_distribution |
(latents, figsize=(12, 4)) |
Per-dimension mean ± std and a histogram of all values |
Training curves — visualization/training_curves.py¶
| Function | Signature (defaults) | Plots |
|---|---|---|
plot_training_curves |
(train_losses, val_losses, metric_name="Loss", figsize=(10, 6)) |
Train vs val per epoch |
plot_multiple_metrics |
(metrics_dict, figsize=(15, 10)) |
{"metric": {"train": [...], "val": [...]}} in a 2-column grid |
plot_learning_rate_schedule |
(learning_rates, figsize=(10, 4)) |
LR per epoch, log scale |
Example: inspect a trained VAE¶
import torch
from ecgen.families.vae import VAELightning
from ecgen.data.datasets import MIMICIVECG
from visualization import plot_ecg_leads, plot_ecg_comparison, plot_latent_space_2d
model = VAELightning.load_from_checkpoint("runs/vae_mimic/seed_42/checkpoints/last.ckpt", map_location="cpu")
model.eval()
ds = MIMICIVECG(root="/path/to/mimic-iv-ecg", split="test", max_samples=64)
x = torch.stack([ds[i]["signal"] for i in range(64)]) # (64, 12, 5000)
recon = model.reconstruct(x)
plot_ecg_comparison(x[0], recon[0], lead_idx=1, lead_name="II").savefig("lead_II.png")
plot_ecg_leads(model.sample(n_samples=1)[0], title="VAE sample").savefig("sample.png")
latents = model.vae.encode_to_latent(x) # (64, 8, 625)
plot_latent_space_2d(latents, method="pca").savefig("latent_pca.png")