Download scripts/RadIR_Trainer_CXR_CT.py from AIGeeksGroup/HMCLIP: direct link, hf CLI and curl.
- Browser
- Download file 16.4 kB
-
https://huggingface.co/AIGeeksGroup/HMCLIP/resolve/main/scripts/RadIR_Trainer_CXR_CT.py
- Command line
-
hf download hf://AIGeeksGroup/HMCLIP/scripts/RadIR_Trainer_CXR_CT.py
-
curl -L -o RadIR_Trainer_CXR_CT.py https://huggingface.co/AIGeeksGroup/HMCLIP/resolve/main/scripts/RadIR_Trainer_CXR_CT.py
16.4 kB
| import os | |
| from pathlib import Path | |
| from datetime import timedelta | |
| import time | |
| from datetime import datetime | |
| import wandb | |
| import torch | |
| from torch import nn | |
| from torch.utils.data import DataLoader | |
| from torch.utils.tensorboard import SummaryWriter | |
| from accelerate import Accelerator | |
| from accelerate.utils import DistributedDataParallelKwargs, InitProcessGroupKwargs | |
| from transformer_maskgit.transformer_maskgit.optimizer import get_optimizer | |
| from scripts.scheduler import cosine_lr | |
| # === 导入两个 Dataset === | |
| from Rad_IR.radir.data.hmclip_mimic_box import HyCoClipMimicBoxDataset_JPG, collate_hmclip_box | |
| def cycle(dl): | |
| """让 DataLoader 无限循环""" | |
| while True: | |
| for data in dl: | |
| yield data | |
| class AverageMeter: | |
| """统计均值的工具类""" | |
| def __init__(self): | |
| self.reset() | |
| def reset(self): | |
| self.sum = 0 | |
| self.count = 0 | |
| self.val = 0 | |
| self.avg = 0 | |
| def update(self, val, n=1): | |
| """更新一次计数""" | |
| self.val = float(val) | |
| self.sum += float(val) * n | |
| self.count += n | |
| self.avg = self.sum / self.count if self.count > 0 else 0.0 | |
| class RadIRTrainer(nn.Module): | |
| """ | |
| RadIR 模型训练器: | |
| - 自动支持 Stage1(Box 监督) | |
| """ | |
| def __init__( | |
| self, | |
| RADIR: nn.Module, | |
| *, | |
| tokenizer, | |
| train_dataset, | |
| valid_dataset, | |
| num_train_steps, | |
| current_step=0, | |
| warmup_steps=1000, | |
| batch_size=32, | |
| lr=5e-5, | |
| wd=0.0, | |
| max_grad_norm=0.5, | |
| save_results_every=500, | |
| save_model_every=2000, | |
| results_folder="./outputs", | |
| num_workers=4, | |
| pin_memory=False, | |
| accelerate_kwargs=dict(), | |
| modal_embedding=False, | |
| debug=False, | |
| ): | |
| super().__init__() | |
| self.best_val_loss = float('inf') | |
| self.debug = debug | |
| self.modal_embedding = modal_embedding | |
| self.save_results_every = save_results_every | |
| self.save_model_every = save_model_every | |
| # ------------------------- | |
| # 判断当前是 Stage1 | |
| # ------------------------- | |
| self.is_stage1 = True # 只用 Stage1 | |
| print(f"\n[Trainer 初始化] 当前模式: Stage1(Simple)\n") | |
| # ------------------------- | |
| # 初始化 Accelerator(DDP + 混合精度) | |
| # ------------------------- | |
| ddp_kwargs = DistributedDataParallelKwargs(find_unused_parameters=True) | |
| pg_kwargs = InitProcessGroupKwargs(timeout=timedelta(seconds=36000)) | |
| self.accelerator = Accelerator( | |
| mixed_precision="no", | |
| kwargs_handlers=[ddp_kwargs, pg_kwargs], | |
| **accelerate_kwargs, | |
| ) | |
| self.Rad_IR = RADIR | |
| self.tokenizer = tokenizer | |
| self.device = self.accelerator.device | |
| # ------------------------- | |
| # 基本训练参数 | |
| # ------------------------- | |
| self.num_train_steps = int(num_train_steps) | |
| self.current_step = int(current_step) | |
| self.batch_size = batch_size | |
| self.max_grad_norm = max_grad_norm | |
| # ------------------------- | |
| # 优化器 + 学习率调度 | |
| # ------------------------- | |
| all_params = list(self.Rad_IR.parameters()) | |
| init_lr = lr[0] if isinstance(lr, (list, tuple)) else float(lr) | |
| self.optim = get_optimizer(all_params, lr=init_lr, wd=wd) | |
| self.lr_scheduler = cosine_lr( | |
| self.optim, init_lr, warmup_steps, self.num_train_steps | |
| ) | |
| # ------------------------- | |
| # DataLoader(根据 stage 自动切换 collate) | |
| # ------------------------- | |
| collate_fn = collate_hmclip_box | |
| self.train_dl = DataLoader( | |
| train_dataset, | |
| batch_size=batch_size, | |
| shuffle=True, | |
| num_workers=num_workers, | |
| pin_memory=pin_memory, | |
| drop_last=True, | |
| collate_fn=collate_fn, | |
| ) | |
| self.valid_dl = DataLoader( | |
| valid_dataset, | |
| batch_size=batch_size, | |
| shuffle=False, | |
| num_workers=num_workers, | |
| pin_memory=pin_memory, | |
| drop_last=False, | |
| collate_fn=collate_fn, | |
| ) | |
| # ------------------------- | |
| # Accelerator 包装模型、优化器、DataLoader | |
| # ------------------------- | |
| ( | |
| self.Rad_IR, | |
| self.optim, | |
| self.train_dl, | |
| self.valid_dl, | |
| ) = self.accelerator.prepare( | |
| self.Rad_IR, | |
| self.optim, | |
| self.train_dl, | |
| self.valid_dl, | |
| ) | |
| self.train_iter = cycle(self.train_dl) | |
| # ------------------------- | |
| # 日志 & 输出目录 | |
| # ------------------------- | |
| self.results_folder = Path(results_folder) | |
| self.results_folder.mkdir(parents=True, exist_ok=True) | |
| tb_path = self.results_folder / "tensorboard" | |
| tb_path.mkdir(parents=True, exist_ok=True) | |
| self.tb_writer = SummaryWriter(str(tb_path)) | |
| # ------------------------- | |
| # 统计指标 | |
| # ------------------------- | |
| self.uncon_loss_m = AverageMeter() # 总 loss | |
| self.it_ce_loss_m = AverageMeter() # image-text CE | |
| self.it_entailment_loss_m = AverageMeter() # image-text entailment | |
| self.ii_text_image_entailment_loss_m = AverageMeter() # image-image entailment | |
| print(f"[Trainer] 已准备完毕,总训练步数:{self.num_train_steps}") | |
| # ------------------------- | |
| # 时间统计 | |
| # ------------------------- | |
| self.train_start_time = None | |
| self.step_time_m = AverageMeter() | |
| # ======================================================= | |
| # 前向:根据 Stage 自动切换分支 | |
| # ======================================================= | |
| def _forward_batch(self, batch): | |
| imgs = batch["imgs"].to(self.device) | |
| caption_ids = batch["caption_ids"].to(self.device) | |
| token_type_ids = batch["token_type_ids"].to(self.device) | |
| attention_mask = batch["attention_mask"].to(self.device) | |
| phrase_text_mask = batch["phrase_text_mask"].to(self.device) | |
| phrases_bboxes_mask = batch["phrases_bboxes_mask"].to(self.device) | |
| text_batch = { | |
| "caption_ids": caption_ids, | |
| "token_type_ids": token_type_ids, | |
| "attention_mask": attention_mask, | |
| } | |
| loss, ce_loss, entail_loss, global_entail_loss = self.Rad_IR( | |
| text=text_batch, | |
| image=imgs, | |
| device=self.device, | |
| is_condition=False, | |
| return_loss=True, | |
| modal_embedding=self.modal_embedding, | |
| phrase_text_mask=phrase_text_mask, | |
| phrases_bboxes_mask=phrases_bboxes_mask, | |
| return_latents=True, | |
| ) | |
| return loss, ce_loss, entail_loss, global_entail_loss | |
| # ======================================================= | |
| # 主训练循环 | |
| # ======================================================= | |
| def train_model(self, resume_path=None): | |
| # ====================== | |
| # 先尝试 Resume 恢复训练 | |
| # ====================== | |
| if resume_path is not None: | |
| self.load_checkpoint(resume_path) | |
| self.Rad_IR.train() | |
| self.train_start_time = time.time() | |
| torch.autograd.set_detect_anomaly(True) | |
| for step in range(self.current_step, self.num_train_steps): | |
| self.current_step = step | |
| step_start_time = time.time() | |
| self.lr_scheduler.step() | |
| # ============================ | |
| # ✅ Stage 1 | |
| # ============================ | |
| with self.accelerator.accumulate(self.Rad_IR): | |
| batch = next(self.train_iter) | |
| loss, ce_loss, entail_loss, global_entail_loss = self._forward_batch(batch) | |
| #加检查1 | |
| for name, val in { | |
| "loss": loss, | |
| "ce": ce_loss, | |
| "entail": entail_loss, | |
| "global_entail": global_entail_loss, | |
| }.items(): | |
| if not torch.isfinite(val): | |
| print(f"[NaN LOSS] step={step}, {name}") | |
| self._dump_bad_batch(batch, step) | |
| raise RuntimeError("NaN in loss") | |
| #加检查2 | |
| self.optim.zero_grad() | |
| self.accelerator.backward(loss) | |
| #加检查1 | |
| for n, p in self.Rad_IR.named_parameters(): | |
| if p.grad is not None and not torch.isfinite(p.grad).all(): | |
| print(f"[NaN GRAD] step={step}, param={n}") | |
| raise RuntimeError("NaN in grad") | |
| #加检查2 | |
| # ============================ | |
| # ✅ 只有在 sync_gradients 才 step | |
| # ============================ | |
| if self.accelerator.sync_gradients: | |
| if self.max_grad_norm is not None: | |
| self.accelerator.clip_grad_norm_( | |
| self.Rad_IR.parameters(), self.max_grad_norm | |
| ) | |
| self.optim.step() | |
| self.optim.zero_grad(set_to_none=True) | |
| # ============================ | |
| # ✅ 时间统计 | |
| # ============================ | |
| if torch.cuda.is_available(): | |
| torch.cuda.synchronize() | |
| step_time = time.time() - step_start_time | |
| self.step_time_m.update(step_time) | |
| # ✅ loss 记录(已有的 AverageMeter) | |
| # ===== 从 GPU 拿回 4 个 loss ===== | |
| loss_val = float(loss.detach().cpu()) | |
| ce_loss_val = float(ce_loss.detach().cpu()) | |
| entail_loss_val = float(entail_loss.detach().cpu()) | |
| ii_entail_loss_val = float(global_entail_loss.detach().cpu()) | |
| # ===== 更新 4 个 meter ===== | |
| self.uncon_loss_m.update(loss_val, 1) | |
| self.it_ce_loss_m.update(ce_loss_val, 1) | |
| self.it_entailment_loss_m.update(entail_loss_val, 1) | |
| self.ii_text_image_entailment_loss_m.update(ii_entail_loss_val, 1) | |
| # ============================ | |
| # ✅ 日志 | |
| # ============================ | |
| if self.accelerator.is_main_process and (step + 1) % 10 == 0: | |
| eta_seconds = int(self.step_time_m.avg * (self.num_train_steps - step - 1)) | |
| eta_str = str(timedelta(seconds=eta_seconds)) | |
| print( | |
| f"[Step {step+1}/{self.num_train_steps}] " | |
| f"loss={self.uncon_loss_m.avg:.4f}, " | |
| f"ce={self.it_ce_loss_m.avg:.4f}, " | |
| f"entail={self.it_entailment_loss_m.avg:.4f}, " | |
| f"step_time={self.step_time_m.avg:.3f}s, ETA={eta_str}" | |
| ) | |
| wandb.log({ | |
| "train/loss": self.uncon_loss_m.avg, | |
| "train/ce_loss": self.it_ce_loss_m.avg, | |
| "train/entail_loss": self.it_entailment_loss_m.avg, | |
| "train/global_entail_loss": self.ii_text_image_entailment_loss_m.avg, | |
| "lr": self.optim.param_groups[0]["lr"], | |
| "step_time": self.step_time_m.avg, | |
| "step": step + 1 | |
| }, step=step + 1) | |
| # ============================ | |
| # ✅ 验证 | |
| # ============================ | |
| if (step + 1) % self.save_results_every == 0: | |
| val_loss = self.evaluate(step + 1) | |
| # ========= 更新 best ========= | |
| if self.accelerator.is_main_process: | |
| if val_loss < self.best_val_loss: | |
| self.best_val_loss = val_loss | |
| self.save_checkpoint(step + 1, is_best=True) | |
| # ============================ | |
| # ✅ 存模型 | |
| # ============================ | |
| if (step + 1) % self.save_model_every == 0: | |
| self.save_checkpoint(step + 1) | |
| # ✅ 最终评估 + 保存 | |
| self.evaluate(self.num_train_steps) | |
| self.save_checkpoint(self.num_train_steps, is_final=True) | |
| # ======================================================= | |
| # 验证集评估 | |
| # ======================================================= | |
| def evaluate(self, step): | |
| self.Rad_IR.eval() | |
| eval_start_time = time.time() # ✅ 验证开始计时 | |
| loss_m = AverageMeter() | |
| ce_m = AverageMeter() | |
| entail_m = AverageMeter() | |
| g_entail_m = AverageMeter() | |
| for batch in self.valid_dl: | |
| loss, ce_loss, entail_loss, g_entail_loss = self._forward_batch(batch) | |
| bs = batch["imgs"].size(0) | |
| loss_m.update(loss.item(), bs) | |
| ce_m.update(ce_loss.item(), bs) | |
| entail_m.update(entail_loss.item(), bs) | |
| g_entail_m.update(g_entail_loss.item(), bs) | |
| if self.accelerator.is_main_process: | |
| print( | |
| f"[Eval step {step}] val_loss={loss_m.avg:.4f}, " | |
| f"val_ce={ce_m.avg:.4f}, val_entail={entail_m.avg:.4f}" | |
| ) | |
| self.tb_writer.add_scalar("val/loss", loss_m.avg, step) | |
| self.tb_writer.add_scalar("val/ce_loss", ce_m.avg, step) | |
| self.Rad_IR.train() | |
| eval_time = time.time() - eval_start_time | |
| eval_time_str = str(timedelta(seconds=int(eval_time))) | |
| if self.accelerator.is_main_process: | |
| print(f"[Eval Done] step={step} 耗时:{eval_time_str}") | |
| self.tb_writer.add_scalar("time/eval_time", eval_time, step) | |
| wandb.log({ | |
| "val/loss": loss_m.avg, | |
| "val/ce_loss": ce_m.avg, | |
| "val/entail_loss": entail_m.avg, | |
| "val/global_entail_loss": g_entail_m.avg, | |
| }, step=step) | |
| return loss_m.avg | |
| # ======================================================= | |
| # 模型保存 | |
| # ======================================================= | |
| def save_checkpoint(self, step, is_final=False, is_best=False): | |
| if not self.accelerator.is_main_process: | |
| return | |
| if is_final: | |
| ckpt_name = "radir_final.pt" | |
| elif is_best: | |
| ckpt_name = "radir_best.pt" | |
| else: | |
| ckpt_name = "radir_last.pt" # 覆盖最新 | |
| ckpt_path = self.results_folder / ckpt_name | |
| state = { | |
| "model": self.accelerator.get_state_dict(self.Rad_IR), | |
| "optimizer": self.optim.state_dict(), | |
| "scheduler": self.lr_scheduler.state_dict(), | |
| "step": step, | |
| "best_val_loss": self.best_val_loss, | |
| } | |
| torch.save(state, ckpt_path) | |
| print(f"[Checkpoint] 保存: {ckpt_path}") | |
| # ======================================================= | |
| # 模型恢复 | |
| # ======================================================= | |
| def load_checkpoint(self, ckpt_path): | |
| if not os.path.exists(ckpt_path): | |
| raise FileNotFoundError(f"Checkpoint 不存在: {ckpt_path}") | |
| print(f"[Resume] 从 {ckpt_path} 恢复训练") | |
| ckpt = torch.load(ckpt_path, map_location="cpu") | |
| # 1️⃣ 恢复模型 | |
| self.accelerator.unwrap_model(self.Rad_IR).load_state_dict(ckpt["model"]) | |
| # 2️⃣ 恢复优化器 | |
| self.optim.load_state_dict(ckpt["optimizer"]) | |
| # 3️⃣ 恢复 scheduler(如果存在) | |
| if "scheduler" in ckpt: | |
| self.lr_scheduler.load_state_dict(ckpt["scheduler"]) | |
| print("[Resume] Scheduler 已恢复") | |
| else: | |
| print("[Resume] ⚠️ 未发现 scheduler,使用新的 scheduler") | |
| # 4️⃣ 恢复训练进度 | |
| self.current_step = ckpt["step"] | |
| # 5️⃣ 恢复 best_val_loss | |
| self.best_val_loss = ckpt.get("best_val_loss", float("inf")) | |
| print("resume lr =", self.optim.param_groups[0]["lr"]) | |
| print( | |
| f"[Resume] 恢复完成:step = {self.current_step}, " | |
| f"best_val_loss = {self.best_val_loss:.4f}" | |
| ) | |