Download stage1_eva_i2t.py from AIGeeksGroup/HMCLIP: direct link, hf CLI and curl.
- Browser
- Download file 13.6 kB
-
https://huggingface.co/AIGeeksGroup/HMCLIP/resolve/main/stage1_eva_i2t.py
- Command line
-
hf download hf://AIGeeksGroup/HMCLIP/stage1_eva_i2t.py
-
curl -L -o stage1_eva_i2t.py https://huggingface.co/AIGeeksGroup/HMCLIP/resolve/main/stage1_eva_i2t.py
13.6 kB
| from __future__ import annotations | |
| """ | |
| I2T / T2I evaluation with RaTEScore-aligned GT. | |
| Uses valid.jsonl for image-text pairs and maps each sample to val_caption.csv | |
| row indices, then slices val_ratescore.npy into a sub-matrix aligned with the | |
| eval set order (same logic as RadIR evaluate_uncon soft_label path). | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import numpy as np | |
| import pandas as pd | |
| import torch | |
| from torch.utils.data import DataLoader, Subset | |
| from transformers import AutoModel, AutoTokenizer | |
| from Rad_IR.radir import lorentz as L | |
| from Rad_IR.radir.RadIR import RADIR, l2norm | |
| from Rad_IR.radir.data.hmclip_mimic_box import HyCoClipMimicBoxDataset_JPG, collate_hmclip_box | |
| from retrieval_metric import compute_ndcg_uncon, soft_retrieval_uncon | |
| from transformer_maskgit.transformer_maskgit.ctvit import CTViT | |
| ROOT = os.path.dirname(os.path.abspath(__file__)) | |
| # --------------------------------------------------------------------------- | |
| # RaTEScore index alignment (valid.jsonl -> val_caption.csv) | |
| # --------------------------------------------------------------------------- | |
| def study_key_from_csv_path(file_path: str) -> str: | |
| """/p10/p10190940/s51351116.txt -> p10/p10190940/s51351116""" | |
| return os.path.splitext(file_path.lstrip("/"))[0] | |
| def study_key_from_jsonl_img_path(img_path: str) -> str: | |
| """dataset/p10/p10190940/s51351116/xxx.jpg -> p10/p10190940/s51351116""" | |
| p = img_path.replace("\\", "/") | |
| if p.startswith("dataset/"): | |
| p = p[len("dataset/") :] | |
| parts = p.split("/") | |
| if len(parts) < 3: | |
| raise ValueError(f"Cannot parse study key from img_path: {img_path}") | |
| return "/".join(parts[:3]) | |
| def build_eval_index_mapping( | |
| jsonl_path: str, | |
| csv_path: str, | |
| dedupe_study: bool = False, | |
| ): | |
| """ | |
| Map each eval row in jsonl to a row index in val_caption.csv / val_ratescore.npy. | |
| Returns: | |
| keep_row_indices: indices into jsonl/dataset to evaluate (in jsonl order) | |
| csv_indices: parallel list of val_caption.csv row indices for RaTEScore slice | |
| stats: dict with alignment diagnostics | |
| """ | |
| df = pd.read_csv(csv_path) | |
| csv_keys = [study_key_from_csv_path(p) for p in df["File Path"]] | |
| key_to_csv_idx = {k: i for i, k in enumerate(csv_keys)} | |
| jsonl_keys = [] | |
| with open(jsonl_path, encoding="utf-8") as f: | |
| for line in f: | |
| item = json.loads(line.strip()) | |
| jsonl_keys.append(study_key_from_jsonl_img_path(item["img_path"])) | |
| keep_row_indices = [] | |
| csv_indices = [] | |
| seen_study = set() | |
| unmatched = [] | |
| for row_i, key in enumerate(jsonl_keys): | |
| if key not in key_to_csv_idx: | |
| unmatched.append((row_i, key)) | |
| continue | |
| if dedupe_study and key in seen_study: | |
| continue | |
| seen_study.add(key) | |
| keep_row_indices.append(row_i) | |
| csv_indices.append(key_to_csv_idx[key]) | |
| stats = { | |
| "jsonl_rows": len(jsonl_keys), | |
| "csv_rows": len(csv_keys), | |
| "eval_rows": len(keep_row_indices), | |
| "unique_studies": len(seen_study), | |
| "unmatched_jsonl": len(unmatched), | |
| "csv_not_in_jsonl": sum(1 for k in csv_keys if k not in set(jsonl_keys)), | |
| "dedupe_study": dedupe_study, | |
| } | |
| if unmatched: | |
| stats["unmatched_examples"] = unmatched[:5] | |
| return keep_row_indices, csv_indices, stats | |
| def load_aligned_ratescore_gt(gt_path: str, csv_indices: list[int]) -> torch.Tensor: | |
| """Slice full RaTEScore matrix to match eval sample order.""" | |
| gt = np.load(gt_path) | |
| idx = np.asarray(csv_indices, dtype=np.int64) | |
| gt_sub = gt[np.ix_(idx, idx)].astype(np.float32) | |
| return torch.from_numpy(gt_sub / 100.0) | |
| # --------------------------------------------------------------------------- | |
| # Feature extraction (aligned with RadIR.forward is_condition=False global path) | |
| # --------------------------------------------------------------------------- | |
| def preprocess_image_like_forward(image: torch.Tensor) -> torch.Tensor: | |
| if image.ndim == 3: | |
| image = image.unsqueeze(0) | |
| if image.ndim == 4: | |
| if image.shape[1] != 1: | |
| image = image.mean(dim=1, keepdim=True) | |
| elif image.ndim == 5: | |
| if image.shape[1] != 1: | |
| image = image.mean(dim=1, keepdim=True) | |
| else: | |
| raise ValueError(f"Unexpected image ndim={image.ndim}, shape={tuple(image.shape)}") | |
| return image | |
| def extract_hyperbolic_features(model, dataloader, device, modal_embedding=False): | |
| """Extract hyperbolic features (project_img/txt after l2norm global encodings).""" | |
| all_hyp_img, all_hyp_txt = [], [] | |
| model.eval() | |
| with torch.no_grad(): | |
| for batch in dataloader: | |
| imgs = preprocess_image_like_forward(batch["imgs"].to(device, non_blocking=True)) | |
| enc_image = model.visual_transformer( | |
| imgs, | |
| return_encoded_tokens=True, | |
| modal_embedding=modal_embedding, | |
| modal_indexs=None, | |
| is_condition=False, | |
| ) | |
| if enc_image.dim() == 2: | |
| image_global = l2norm(enc_image) | |
| elif enc_image.dim() == 3: | |
| image_global = l2norm(enc_image[:, 0, :]) | |
| else: | |
| raise ValueError(f"Unexpected visual feature shape: {enc_image.shape}") | |
| text_embeddings = model.text_transformer( | |
| input_ids=batch["caption_ids"].to(device, non_blocking=True), | |
| token_type_ids=batch["token_type_ids"].to(device, non_blocking=True), | |
| attention_mask=batch["attention_mask"].to(device, non_blocking=True), | |
| ) | |
| enc_text = text_embeddings[0] | |
| text_global = enc_text[:, 0, :] | |
| all_hyp_img.append(model.project_img(image_global).cpu()) | |
| all_hyp_txt.append(model.project_txt(text_global).cpu()) | |
| return torch.cat(all_hyp_img, dim=0), torch.cat(all_hyp_txt, dim=0) | |
| # --------------------------------------------------------------------------- | |
| # Metrics | |
| # --------------------------------------------------------------------------- | |
| def paired_recall(sim_matrix: torch.Tensor, ks): | |
| """Hard paired recall: query i should retrieve candidate i (diagonal).""" | |
| ranking = torch.argsort(sim_matrix, dim=1, descending=True) | |
| gt = torch.arange(sim_matrix.size(0), device=sim_matrix.device) | |
| results = {} | |
| for k in ks: | |
| hits = (ranking[:, :k] == gt.unsqueeze(1)).any(dim=1).float() | |
| results[k] = hits.mean().item() | |
| return results | |
| def print_metric_block(title, ks, paired_r, soft_r, ndcg, scale_pct=True): | |
| print(f"\n--- {title} ---") | |
| for k in ks: | |
| sr = soft_r[k] if isinstance(soft_r, dict) else soft_r[ks.index(k)] | |
| if sr == -1: | |
| sr_str = "N/A" | |
| else: | |
| sr_str = f"{sr * 100:.2f}%" if scale_pct else f"{sr:.4f}" | |
| nd = ndcg[ks.index(k)] | |
| nd_str = f"{nd * 100:.2f}" if scale_pct else f"{nd:.4f}" | |
| print( | |
| f"@{k:>3d} Paired_R={paired_r[k] * 100:.2f}% " | |
| f"Soft_R={sr_str} NDCG={nd_str}" | |
| ) | |
| def evaluate_i2t_t2i(sim_i2t, gt_ratescore, ks): | |
| """Run RadIR-style I2T metrics with RaTEScore GT matrix.""" | |
| sim_t2i = sim_i2t.t() | |
| gt = gt_ratescore.to(sim_i2t.device) | |
| paired_i2t = paired_recall(sim_i2t, ks) | |
| paired_t2i = paired_recall(sim_t2i, ks) | |
| soft_i2t = soft_retrieval_uncon(sim_i2t, gt, k=ks) | |
| soft_t2i = soft_retrieval_uncon(sim_t2i, gt, k=ks) | |
| if not isinstance(soft_i2t, list): | |
| soft_i2t = [soft_i2t] | |
| soft_t2i = [soft_t2i] | |
| ndcg_i2t = compute_ndcg_uncon(sim_i2t, gt, k=ks) | |
| ndcg_t2i = compute_ndcg_uncon(sim_t2i, gt, k=ks) | |
| soft_i2t = dict(zip(ks, soft_i2t)) | |
| soft_t2i = dict(zip(ks, soft_t2i)) | |
| return { | |
| "i2t": {"paired_r": paired_i2t, "soft_r": soft_i2t, "ndcg": ndcg_i2t}, | |
| "t2i": {"paired_r": paired_t2i, "soft_r": soft_t2i, "ndcg": ndcg_t2i}, | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Main | |
| # --------------------------------------------------------------------------- | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="I2T/T2I evaluation with RaTEScore-aligned GT (valid.jsonl subset)" | |
| ) | |
| parser.add_argument("--device", default="cuda", type=str) | |
| parser.add_argument( | |
| "--model_path", | |
| default=os.path.join( | |
| ROOT, "hf_models/microsoft/BiomedVLP-CXR-BERT-specialized" | |
| ), | |
| ) | |
| parser.add_argument("--radir_ckpt", default=os.path.join(ROOT, "outputs/ablation/CXRCT_stage1_no_cross_text_entail_20260703_153255/radir_best.pt")) | |
| parser.add_argument("--dataset_root", default=os.path.join(ROOT, "dataset")) | |
| parser.add_argument("--caption_file", default="valid.jsonl") | |
| parser.add_argument( | |
| "--csv_file", | |
| default=os.path.join(ROOT, "dataset/val_caption.csv"), | |
| help="Original RadIR caption list (defines val_ratescore row order)", | |
| ) | |
| parser.add_argument( | |
| "--gt_path", | |
| default=os.path.join(ROOT, "dataset/val_ratescore.npy"), | |
| ) | |
| parser.add_argument("--cig_matched_dir", default="dataset/processed/matched") | |
| parser.add_argument("--split", default="valid", type=str) | |
| parser.add_argument("--batch_size", default=16, type=int) | |
| parser.add_argument("--num_workers", default=8, type=int) | |
| parser.add_argument("--imsize", default=224, type=int) | |
| parser.add_argument("--max_words", default=128, type=int) | |
| parser.add_argument( | |
| "--dedupe_study", | |
| action="store_true", | |
| help="Keep one image per study (~2808). Default: all jsonl rows (~3120).", | |
| ) | |
| parser.add_argument( | |
| "--modal_embedding", | |
| action="store_true", | |
| help="Match training when --modal_embedding was enabled", | |
| ) | |
| parser.add_argument( | |
| "--ks", | |
| type=int, | |
| nargs="+", | |
| default=[1, 5, 10, 20, 50, 100], | |
| ) | |
| args = parser.parse_args() | |
| device = args.device | |
| jsonl_path = os.path.join(args.dataset_root, args.caption_file) | |
| keep_rows, csv_indices, stats = build_eval_index_mapping( | |
| jsonl_path, args.csv_file, dedupe_study=args.dedupe_study | |
| ) | |
| if stats["unmatched_jsonl"]: | |
| raise RuntimeError( | |
| f"{stats['unmatched_jsonl']} jsonl rows have no match in {args.csv_file}. " | |
| f"Examples: {stats.get('unmatched_examples')}" | |
| ) | |
| print("===== RaTEScore alignment =====") | |
| for k, v in stats.items(): | |
| if k != "unmatched_examples": | |
| print(f" {k}: {v}") | |
| gt_ratescore = load_aligned_ratescore_gt(args.gt_path, csv_indices) | |
| print(f" gt_sub shape: {tuple(gt_ratescore.shape)}") | |
| tokenizer = AutoTokenizer.from_pretrained( | |
| args.model_path, trust_remote_code=True, local_files_only=True | |
| ) | |
| text_model = AutoModel.from_pretrained( | |
| args.model_path, trust_remote_code=True, local_files_only=True | |
| ) | |
| image_encoder = CTViT( | |
| dim=768, | |
| codebook_size=8192, | |
| image_size=args.imsize, | |
| patch_size=16, | |
| temporal_patch_size=10, | |
| spatial_depth=8, | |
| temporal_depth=6, | |
| cls_depth=4, | |
| dim_head=32, | |
| heads=8, | |
| channels=1, | |
| ) | |
| model = RADIR( | |
| tokenizer=tokenizer, | |
| image_encoder=image_encoder, | |
| text_encoder=text_model, | |
| dim_text=768, | |
| dim_image=768, | |
| dim_latent=768, | |
| use_mlm=False, | |
| use_all_token_embeds=False, | |
| ) | |
| print(f"\nLoading checkpoint: {args.radir_ckpt}") | |
| model.load(args.radir_ckpt) | |
| model.to(device) | |
| full_dataset = HyCoClipMimicBoxDataset_JPG( | |
| split=args.split, | |
| tokenizer=tokenizer, | |
| dataset_root=args.dataset_root, | |
| cig_matched_dir=args.cig_matched_dir, | |
| caption_file=args.caption_file, | |
| imsize=args.imsize, | |
| max_words=args.max_words, | |
| transform=None, | |
| ) | |
| dataset = Subset(full_dataset, keep_rows) | |
| dataloader = DataLoader( | |
| dataset, | |
| batch_size=args.batch_size, | |
| shuffle=False, | |
| num_workers=args.num_workers, | |
| pin_memory=True, | |
| collate_fn=collate_hmclip_box, | |
| ) | |
| print(f"\nEval samples = {len(dataset)} (from {len(full_dataset)} jsonl rows)") | |
| hyp_img, hyp_txt = extract_hyperbolic_features( | |
| model, | |
| dataloader, | |
| device, | |
| modal_embedding=args.modal_embedding, | |
| ) | |
| gt = gt_ratescore.to(device) | |
| ks = args.ks | |
| curv = model.get_curv() | |
| sim_i2t = -L.pairwise_dist( | |
| hyp_img.to(device), hyp_txt.to(device), curv | |
| ).float() | |
| print(f"\n===== Hyperbolic I2T/T2I (curv={curv.item():.4f}) =====") | |
| res = evaluate_i2t_t2i(sim_i2t, gt, ks) | |
| print_metric_block( | |
| "Image -> Text", | |
| ks, | |
| res["i2t"]["paired_r"], | |
| res["i2t"]["soft_r"], | |
| res["i2t"]["ndcg"], | |
| ) | |
| print_metric_block( | |
| "Text -> Image", | |
| ks, | |
| res["t2i"]["paired_r"], | |
| res["t2i"]["soft_r"], | |
| res["t2i"]["ndcg"], | |
| ) | |
| print("\n Summary (mean I2T+T2I):") | |
| for i, k in enumerate(ks): | |
| pr = (res["i2t"]["paired_r"][k] + res["t2i"]["paired_r"][k]) / 2 | |
| nd = (res["i2t"]["ndcg"][i] + res["t2i"]["ndcg"][i]) / 2 | |
| si = res["i2t"]["soft_r"][k] | |
| st = res["t2i"]["soft_r"][k] | |
| sr = (si + st) / 2 if si != -1 and st != -1 else -1 | |
| sr_str = f"{sr * 100:.2f}%" if sr != -1 else "N/A" | |
| print( | |
| f" @{k:>3d} Paired_R={pr * 100:.2f}% " | |
| f"Soft_R={sr_str} NDCG={nd * 100:.2f}" | |
| ) | |
| if __name__ == "__main__": | |
| main() | |