LTAF ECG Beat Classifier β€” HTF (Time + Frequency + History)

A 3-class beat classifier (Normal / Atrial premature / Ventricular premature) trained on PhysioNet's Long-Term Atrial Fibrillation (LTAF) database.

Inspired by the History-Time-Frequency (HTF) ensemble in alberto-rota/PAC-PVC-Beat-Classifier-for-ECGs: three parallel streams (raw waveform, FFT log-magnitude, RR-interval history with previous beat labels) fused before a small MLP head.

Metric Value
Test accuracy 0.954
Test balanced accuracy 0.952
Test macro F1 0.936

vs. frozen Chronos-2 + MLP baseline on the same beats: F1 = 0.906 (+3 pp with 4Γ— the head capacity but no LLM forward at inference).

Per-class F1 (test, n = 57 693 beats from 9 records):

  • N (Normal): 0.976
  • A (Atrial premature, PAC / APC): 0.930
  • V (Ventricular premature, PVC / VE): 0.902

Test confusion matrix (rows = true, cols = pred, order N/A/V):

N: [36778,  406, 1278]   (recall 95.6 %)
A: [   98, 8539,  677]   (recall 91.7 %)
V: [   55,  112, 9750]   (recall 98.3 %)

Architecture

EcgBeatHTFClassifier(num_classes=3, n_channels=2, window_samples=256, history_k=5, history_use_labels=True, time_base_channels=32, freq_base_channels=32, head_hidden=128):

  • Time stream β€” 5 Conv1dβ†’BNβ†’ReLUβ†’MaxPool blocks on a (B, 2, 256) raw R-peak-centered window. Adaptive avg pool β†’ 256-dim feature.
  • Frequency stream β€” 4 conv blocks on log|rFFT| of the time signal (B, 2, 129). Adaptive avg pool β†’ 256-dim feature.
  • History stream β€” K=5 preceding RR intervals (sec) + one-hot of preceding K beat labels β†’ 64-dim MLP feature.
  • Head β€” concat (576-dim) β†’ Linear(128) β†’ ReLU β†’ Dropout β†’ Linear(3).
  • Total parameters: 1,143,939.

Quickstart

pip install torch huggingface_hub numpy
import numpy as np
import torch
from huggingface_hub import hf_hub_download
from model import EcgBeatHTFClassifier, BEAT_CLASS_NAMES

ckpt = hf_hub_download("rmxjck/ltaf-ecg-beats-classifier-htf", "best_classifier.pt")
model = EcgBeatHTFClassifier.load(ckpt, device="cuda")
model.eval()

# Inputs:
# x_time: (B, 2, 256) β€” 2 s @ 128 Hz, R-peak centered, z-scored.
# rr_history: (B, 5) β€” RR intervals (seconds) to preceding K=5 beats.
# label_history: (B, 5) int64 β€” preceding K=5 beat labels (0=N, 1=A, 2=V); -1 = missing.
x_time = torch.randn(1, 2, 256).cuda()
rr = torch.tensor([[0.85, 0.83, 0.87, 0.82, 0.85]]).cuda()
lbl = torch.tensor([[0, 0, 0, 0, 0]]).cuda()  # all N
with torch.no_grad():
    pred = model(x_time, rr, lbl).argmax(-1).item()
print(BEAT_CLASS_NAMES[pred])  # "N", "A", or "V"

For a full beat-sequence example (autoregressive history), see inference.py.

Input format

  • Time signal (B, 2, 256) float32: 2 s @ 128 Hz, 2-lead, centered on the R-peak sample, per-channel z-scored.
  • RR history (B, 5) float32: seconds. rr[k] is the gap between the (k+1)th-prior beat and the kth-prior beat. Use 0.0 for unknown.
  • Label history (B, 5) int64: preceding 5 beat labels (0=N, 1=A, 2=V), most-recent first. Use -1 for unknown / record start.

At inference time, label history can be filled by:

  • The model's own previous predictions (autoregressive β€” see predict_beat_sequence in inference.py).
  • All -1 if you don't have any (still works, slight recall loss).

Training recipe

.venv/bin/python scripts/train_ecg_beat_htf.py \
    --epochs 15 --batch-size 256 --history-k 5 \
    --output-dir results/ecg_classifier/beats_htf
  • Dataset: LTAF beat timelines from nicozumarraga/ltaf-haystack. 67 train / 8 val / 9 test records, deterministic seed 42.
  • Loss: weighted cross-entropy with sqrt-dampened inverse-frequency class weights (cap 10), label smoothing 0.05.
  • N (normal) beats subsampled per epoch to 2 Γ— n_nonN to balance the ~97 % / 1.7 % / 1.5 % class skew.
  • Cosine LR 1e-3 β†’ 0 over 15 epochs. AdamW (wd 1e-4).
  • Best checkpoint by val macro F1 (epoch 9 in our run).
  • Training time on a single H100 80GB: ~22 min.

Source repo: scripts/train_ecg_beat_htf.py and src/models/ts_llm/ecg_beat_htf.py in rmxjck/TSLM-Arena.

What was tried and didn't help

  • Raw Chronos-2 frozen encoder + MLP head: F1 = 0.906 (-3 pp). The HTF ensemble's combination of morphology + spectral + R-R-context beats the foundation-model encoder for this single-beat task.
  • Various history-K values (3, 7, 10): K=5 was best.
  • No-history HTF (no label_history): -1.5 pp F1.
  • No-frequency HTF: -1 pp F1.

Not for clinical use

Research artifact only. Not FDA-cleared. Not suitable for triage, diagnosis, or any patient-facing application.

Citation

@misc{petrutiu2008ltafdb,
  title         = {Abrupt Changes in Fibrillatory Wave Characteristics at the Termination of Paroxysmal Atrial Fibrillation in Humans},
  author        = {Petrutiu, Simona and Sahakian, Alan V. and Swiryn, Steven},
  year          = {2008},
  howpublished  = {PhysioNet},
  url           = {https://physionet.org/content/ltafdb/}
}

The HTF architecture is inspired by: alberto-rota/PAC-PVC-Beat-Classifier-for-ECGs.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support