Download training/train_detector.py from pashaalex/ecg-rectifier: direct link, hf CLI and curl.
- Browser
- Download file 4.98 kB
-
https://huggingface.co/pashaalex/ecg-rectifier/resolve/main/training/train_detector.py
- Command line
-
hf download hf://pashaalex/ecg-rectifier/training/train_detector.py
-
curl -L -o train_detector.py https://huggingface.co/pashaalex/ecg-rectifier/resolve/main/training/train_detector.py
4.98 kB
| #!/usr/bin/env python3 | |
| """Train the grid-node detector (models/detector.onnx) on v5 canvases. | |
| DATASET_REPO=<user>/ecg-canvases-v5 MODEL_REPO=<user>/ecg-models RUN_NAME=detector python3 train_detector.py | |
| Head per token: 4 x 4 cells x (heat logit, offset x, offset y) -> a 28 x 28 cell map of the 224 window. | |
| Loss: CenterNet focal loss on the heat map + L1 of the offsets in the cells holding a node, both | |
| normalised by the number of nodes. The checkpoint with the best validation F1 is kept as best.pt. | |
| """ | |
| import math | |
| import os | |
| import numpy as np | |
| import torch | |
| from torch import nn | |
| import common | |
| import crops | |
| import detector_data as dd | |
| DATASET_REPO = os.environ.get("DATASET_REPO", "") | |
| MODEL_REPO = os.environ.get("MODEL_REPO", "") | |
| RUN_NAME = os.environ.get("RUN_NAME", "detector") | |
| EPOCHS = int(os.environ.get("EPOCHS", "156")) | |
| EVAL_EVERY = 2 | |
| VAL_SAMPLES = 1000 | |
| SUB = crops.PATCH_SIZE // dd.CELL | |
| HEAD_HIDDEN = 384 | |
| FOCAL_ALPHA, FOCAL_BETA = 2.0, 4.0 | |
| HEAT_PRIOR = 0.05 | |
| THRESHOLDS = (0.2, 0.3, 0.4, 0.5) | |
| MATCH_RADIUS_PX = 4.0 | |
| class PointViT(common.WindowTransformer): | |
| def __init__(self): | |
| super().__init__() | |
| self.head = nn.Sequential(nn.Linear(common.EMBED_DIM, HEAD_HIDDEN), nn.GELU(), nn.Linear(HEAD_HIDDEN, SUB * SUB * 3)) | |
| self.init_weights(self.head) | |
| with torch.no_grad(): | |
| self.head[-1].bias.view(SUB * SUB, 3)[:, 0] = -math.log((1 - HEAT_PRIOR) / HEAT_PRIOR) | |
| def forward(self, images): | |
| """-> [B, 28, 28, 3] (heat logit, offset logits).""" | |
| b, g = images.shape[0], common.GRID_SIZE | |
| out = self.head(self.tokens(images)).view(b, g, g, SUB, SUB, 3) | |
| return out.permute(0, 1, 3, 2, 4, 5).reshape(b, dd.CELLS, dd.CELLS, 3) | |
| def loss_fn(output, target): | |
| logits, offsets = output[..., 0].float(), torch.sigmoid(output[..., 1:3].float()) | |
| heat, positive = target[..., dd.CH_HEAT], target[..., dd.CH_POS] | |
| count = positive.sum().clamp_min(1.0) | |
| p = torch.sigmoid(logits) | |
| pos_term = -((1 - p) ** FOCAL_ALPHA) * nn.functional.logsigmoid(logits) * positive | |
| neg_term = -((1 - heat) ** FOCAL_BETA) * (p ** FOCAL_ALPHA) * nn.functional.logsigmoid(-logits) * (1 - positive) | |
| focal = (pos_term.sum() + neg_term.sum()) / count | |
| offset = (torch.abs(offsets - target[..., dd.CH_OFF_X:dd.CH_OFF_Y + 1]).sum(-1) * positive).sum() / count | |
| return focal + offset | |
| def validate(model, loader, device): | |
| model.eval() | |
| heat, offsets, targets, kinds = [], [], [], [] | |
| for images, target, kind in loader: | |
| with common.autocast(device): | |
| output = model(crops.normalize_images(images.to(device, non_blocking=True))).float() | |
| heat.append(torch.sigmoid(output[..., 0]).cpu().numpy()) | |
| offsets.append(torch.sigmoid(output[..., 1:3]).cpu().numpy()) | |
| targets.append(target.numpy()) | |
| kinds.append(kind.numpy()) | |
| return dd.f1_scores(np.concatenate(heat), np.concatenate(offsets), np.concatenate(targets), np.concatenate(kinds), | |
| THRESHOLDS, MATCH_RADIUS_PX) | |
| def main(): | |
| common.set_seed() | |
| device = common.device_or_fail() | |
| splits, data = common.prepare_data(DATASET_REPO) | |
| train_loader = common.make_loader(dd.PointDataset(data, dd.train_selection(data, splits["train"])), True, device) | |
| val_loader = common.make_loader(dd.PointDataset(data, splits["val"][:VAL_SAMPLES], train=False), False, device) | |
| model = PointViT().to(device) | |
| optimizer, scheduler = common.make_optimizer(model, len(train_loader), EPOCHS) | |
| run = common.Run(RUN_NAME, MODEL_REPO) | |
| first, best = run.resume(model, optimizer, scheduler, device) | |
| best = -1.0 if best is None else best | |
| for epoch in range(first, EPOCHS + 1): | |
| model.train() | |
| loss_sum, batches = 0.0, 0 | |
| for images, target, _ in train_loader: | |
| images = crops.normalize_images(images.to(device, non_blocking=True)) | |
| target = target.to(device, non_blocking=True) | |
| optimizer.zero_grad(set_to_none=True) | |
| with common.autocast(device): | |
| output = model(images) | |
| loss = loss_fn(output, target) | |
| loss.backward() | |
| nn.utils.clip_grad_norm_(model.parameters(), common.GRADIENT_CLIP_NORM) | |
| optimizer.step() | |
| scheduler.step() | |
| loss_sum, batches = loss_sum + loss.item(), batches + 1 | |
| row = {"epoch": epoch, "train_loss": loss_sum / batches, "val_f1": ""} | |
| if epoch % EVAL_EVERY == 0 or epoch == EPOCHS: | |
| row["val_f1"] = f1 = validate(model, val_loader, device)["best_f1"] | |
| if f1 > best: | |
| best = f1 | |
| run.save("best.pt", model, optimizer, scheduler, epoch, best) | |
| row["best_f1"] = best | |
| run.save("last.pt", model, optimizer, scheduler, epoch, best) | |
| common.log(run, row) | |
| if epoch % 10 == 0 or epoch == EPOCHS: | |
| run.push("epoch {}".format(epoch)) | |
| if __name__ == "__main__": | |
| main() | |