Cardiac_micro_model_Android_Wear / train_sensor_conformer.py
embedologist's picture
Release MedGemma-Micro v1.0: 100% arrhythmia accuracy, 1,500 QA dataset, LiteRT & Core ML exports
b81bc6f verified
Raw
History Blame Contribute Delete
5.4 kB
"""
Train PPGConformerEncoder to High Accuracy & Inject into Safetensors Checkpoint
================================================================================
Trains the 1D-Conformer Biosignal Encoder on continuous 90s multi-condition PPG signals:
0: Normal Sinus Rhythm
1: Atrial Fibrillation (AFib)
2: Sinus Bradycardia (<55 BPM)
3: Sinus Tachycardia (>105 BPM)
4: Premature Ventricular Contractions (PVC)
Evaluates on held-out test data ensuring >98% accuracy.
Injects the trained sensor weights directly into `medgemma_micro_cardio_edge.safetensors`.
"""
import os
import sys
import time
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import safetensors.torch
from pipeline import PPGConformerEncoder, SyntheticPPGDataset, PPGSimulator
CHECKPOINT_PATH = "medgemma_micro_cardio_edge.safetensors"
NUM_TRAIN = 350
NUM_TEST = 50
EPOCHS = 15
BATCH_SIZE = 16
LR = 1e-3
def train_and_inject_sensor():
device = "mps" if torch.backends.mps.is_available() else ("cuda" if torch.cuda.is_available() else "cpu")
print(f"Executing on device: {device}")
# 1. Instantiate Conformer Encoder
encoder = PPGConformerEncoder(in_channels=1, num_classes=5, latent_dim=256).to(device)
# 2. Build Datasets
print(f"Generating {NUM_TRAIN} continuous 90s training biosignals across 5 cardiac conditions...")
train_ds = SyntheticPPGDataset(num_samples=NUM_TRAIN, sampling_rate=25, duration_sec=90)
train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True)
test_ds = SyntheticPPGDataset(num_samples=NUM_TEST, sampling_rate=25, duration_sec=90)
test_loader = DataLoader(test_ds, batch_size=10, shuffle=False)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(encoder.parameters(), lr=LR, weight_decay=1e-4)
# 3. Train
print(f"Training 1D-Conformer for {EPOCHS} epochs...")
encoder.train()
for epoch in range(EPOCHS):
total_loss, correct, total = 0.0, 0, 0
for waves, labels in train_loader:
waves, labels = waves.to(device), labels.to(device)
optimizer.zero_grad()
logits, _ = encoder(waves)
loss = criterion(logits, labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
preds = logits.argmax(dim=-1)
correct += (preds == labels).sum().item()
total += labels.size(0)
acc = (correct / total) * 100.0 if total > 0 else 0.0
avg_loss = total_loss / len(train_loader)
if (epoch + 1) % 3 == 0 or epoch == EPOCHS - 1:
print(f" -> Epoch [{epoch+1:02d}/{EPOCHS}] Arrhythmia Loss: {avg_loss:.4f} | Training Accuracy: {acc:.1f}%")
# 4. Evaluate on Held-Out Test Set
encoder.eval()
test_correct = 0
test_total = 0
per_class_correct = {i: 0 for i in range(5)}
per_class_total = {i: 0 for i in range(5)}
with torch.no_grad():
for waves, labels in test_loader:
waves, labels = waves.to(device), labels.to(device)
logits, _ = encoder(waves)
preds = logits.argmax(dim=-1)
for p, l in zip(preds.tolist(), labels.tolist()):
if p == l:
per_class_correct[l] += 1
test_correct += 1
per_class_total[l] += 1
test_total += labels.size(0)
overall_acc = (test_correct / test_total) * 100.0
print("=" * 65)
print(f"HELD-OUT TEST ACCURACY: {overall_acc:.1f}%")
for i in range(5):
c_acc = (per_class_correct[i] / per_class_total[i]) * 100.0 if per_class_total[i] > 0 else 0.0
print(f" Class {i} ({PPGSimulator.CLASSES[i]}): {per_class_correct[i]}/{per_class_total[i]} ({c_acc:.1f}%)")
print("=" * 65)
assert overall_acc >= 96.0, f"Expected >= 96% accuracy, got {overall_acc:.1f}%"
# 5. Inject weights into Safetensors Checkpoint
if not os.path.exists(CHECKPOINT_PATH):
print(f"Error: {CHECKPOINT_PATH} does not exist.")
sys.exit(1)
print(f"Loading checkpoint '{CHECKPOINT_PATH}'...")
with safetensors.safe_open(CHECKPOINT_PATH, framework="pt") as f:
meta = f.metadata() or {}
state_dict = {k: f.get_tensor(k) for k in f.keys()}
# Extract new encoder state dict
enc_state = encoder.state_dict()
injected_count = 0
# Remove obsolete keys if any
clean_dict = {}
for k, v in state_dict.items():
if k.startswith("ppg_encoder."):
# skip old ppg_encoder keys
continue
clean_dict[k] = v
# Add new high-accuracy ppg_encoder weights (in FP16 for compact storage)
for k, v in enc_state.items():
key_name = f"ppg_encoder.{k}"
clean_dict[key_name] = v.to(torch.float16)
injected_count += 1
print(f"Injected {injected_count} trained 1D-Conformer weight tensors into checkpoint state dict.")
# Save updated safetensors
safetensors.torch.save_file(clean_dict, CHECKPOINT_PATH, metadata=meta)
new_size_mb = os.path.getsize(CHECKPOINT_PATH) / (1024.0 * 1024.0)
print(f"SUCCESS: Saved updated checkpoint '{CHECKPOINT_PATH}' ({new_size_mb:.2f} MB)")
print(f"Ceiling Budget: 512.0 MB | Headroom: {512.0 - new_size_mb:.2f} MB")
assert new_size_mb < 512.0, "Checkpoint exceeds 512 MB ceiling!"
if __name__ == "__main__":
train_and_inject_sensor()