File size: 31,688 Bytes
a2ffd07 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 | """
Unified hallucination-removal validation script.
Supports any scene→object relation defined in experiment/config/relations.json.
Use --relation to select (default: bathroom_toilet).
Supported model types (--model_type):
lora -> loads base model + LoRA adapter via PeftModel
merged -> loads merged HF model via AutoModelForPreTraining
delta_w -> loads HookedSAELlavaConditionalGeneration + .pt state dict
grace -> loads base model + restores GRACE codebook adapters
wise -> loads base model + restores WISE adapter state
dualedit -> loads base model + restores DualEdit adapters
visedit -> loads editor with trained checkpoint
Mention detection (--mention_method):
keyword -> fast negation-aware regex (same as old scripts)
llm -> local LLM judge only
both -> keyword + LLM side-by-side
Usage:
# LoRA adapter with custom relation
python -m experiment.evaluation.validate \
--relation kitchen_microwave \
--model_type lora \
--model_dir step3_lora_v5_outputs/kitchen_microwave/run_xxx/lora_adapter
# Default (bathroom_toilet) for backward compat
python -m experiment.evaluation.validate \
--model_type lora \
--model_dir step3_lora_outputs/lora_adapter
"""
from __future__ import annotations
import os
import sys
import csv
import json
import math
import argparse
from pathlib import Path
# Must be set before importing vllm — subprocess inherits env at spawn time
os.environ["VLLM_USE_V1"] = "0"
os.environ.setdefault("NCCL_P2P_DISABLE", "1")
os.environ.setdefault("NCCL_IB_DISABLE", "1")
import torch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../.."))
from transformers import AutoProcessor, AutoModelForPreTraining
from experiment.config.relation_config import get_relation_config, RelationConfig
from experiment.data.datasets import get_split_image_ids
from experiment.data.hf_loader import load_hf_dataset
from experiment.evaluation.metrics import build_metrics
from experiment.evaluation.inference import (
collect_outputs_transformers,
collect_outputs_vllm,
collect_outputs_visedit,
)
from experiment.evaluation.metric import evaluate_collected_outputs, compute_kme_metrics
from experiment.evaluation.metrics import TextSimilarityScorer
from experiment.evaluation.summary import print_summary
MODEL_NAME = "llava-hf/llava-1.5-7b-hf"
dtype = torch.float16
def parse_args():
parser = argparse.ArgumentParser(description="Unified hallucination-removal validation")
parser.add_argument("--relation", type=str, default="bathroom_toilet",
help="Relation key from relations.json (default: bathroom_toilet)")
parser.add_argument("--val_csv", type=str, default=None,
help="(Legacy) Path to CSV. If omitted, loads from HuggingFace.")
parser.add_argument("--val_image_dir", type=str, default=None,
help="(Legacy) Image directory. If omitted, loads from HuggingFace.")
parser.add_argument("--dataset_id", type=str, default=None,
help="HuggingFace dataset ID (default: auto from relation config)")
parser.add_argument("--num_per_category", type=int, default=50)
parser.add_argument("--use_val_split", action="store_true",
help="Only use val-split images")
parser.add_argument("--prompts", type=str, nargs="+",
default=["Describe this image.", "What do you see in this image?"])
parser.add_argument("--generality_prompts", type=str, nargs="*",
default=["Give a detailed description of this image."],
help="Unseen prompts for generality evaluation (not used in training)")
parser.add_argument("--train_prompts", type=str, nargs="*",
default=None,
help="Prompts that were used during training (default: from relation config)")
parser.add_argument("--max_new_tokens", type=int, default=300)
parser.add_argument("--model_type", type=str,
choices=["lora", "merged", "delta_w", "grace", "wise", "dualedit", "visedit"],
required=True,
help="How to load the finetuned model")
parser.add_argument("--base_model_name", type=str, default=MODEL_NAME,
help="HuggingFace model name for the base / original model")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--model_dir", type=str,
help="Path to merged model or LoRA adapter dir (lora / merged)")
group.add_argument("--checkpoint", type=str,
help="Path to .pt state dict (delta_w)")
parser.add_argument("--mention_method", type=str,
choices=["keyword", "llm", "both"], default="both")
parser.add_argument("--clip_model", type=str, default="google/siglip-base-patch16-224")
parser.add_argument("--judge_model", type=str, default="Qwen/Qwen3-VL-32B-Instruct")
parser.add_argument("--judge_device", type=str, default="cuda",
help="Device for judge, e.g. cuda, cuda:1, or cpu")
parser.add_argument("--judge_max_tokens", type=int, default=150)
parser.add_argument("--inference_backend", type=str, choices=["transformers", "vllm"],
default="transformers")
parser.add_argument("--vllm_batch_size", type=int, default=64)
parser.add_argument("--vllm_tensor_parallel_size", type=int, default=1)
parser.add_argument("--vllm_gpu_memory_utilization", type=float, default=0.9)
parser.add_argument("--vllm_max_model_len", type=int, default=4096)
parser.add_argument("--output_dir", type=str, default="./step4_v2_outputs")
parser.add_argument("--original_cache_dir", type=str,
default="./cached_original_outputs",
help="Directory to cache original model outputs for reuse")
parser.add_argument("--edit_image_ids", type=str, default=None,
help="(deprecated, ignored) Previously pinned BNT eval to specific IDs.")
parser.add_argument("--edit_targets", type=str, default=None,
help="(visedit only) Path to eval_targets.json {image_id: target_new} "
"written by run_visedit.py. Used as correction target for edit signal.")
parser.add_argument("--visedit_dir", type=str, default=None,
help="(visedit only) Path to VisEdit repo root. "
"Defaults to <project_root>/VisEdit.")
return parser.parse_args()
def load_category_images(relation_config: RelationConfig,
num_per_category, use_val_split=False,
dataset_id=None,
csv_path=None, image_dir=None,
edit_image_ids=None):
"""Load first num_per_category images per category deterministically."""
dataset_id = dataset_id or relation_config.dataset_id
scene_col = relation_config.scene_key
object_col = relation_config.object_key
categories = {name: [] for name in relation_config.category_names}
if csv_path is not None and image_dir is not None:
# Legacy CSV loading
val_ids = get_split_image_ids(csv_path, "val") if use_val_split else None
with open(csv_path, "r") as f:
reader = csv.DictReader(f)
for row in reader:
if val_ids is not None and row["image_id"] not in val_ids:
continue
image_path = os.path.join(image_dir, f"{row['image_id']}.jpg")
if not os.path.exists(image_path):
continue
entry = {"path": image_path, "image_id": row["image_id"]}
scene_val = int(row.get(scene_col, 0))
object_val = int(row.get(object_col, 0))
cat = _classify(scene_val, object_val, relation_config)
if cat in categories and len(categories[cat]) < num_per_category:
categories[cat].append(entry)
if all(len(v) >= num_per_category for v in categories.values()):
break
else:
# HuggingFace dataset
if use_val_split:
ds = load_hf_dataset(dataset_id, split="val")
else:
ds = load_hf_dataset(dataset_id)
if hasattr(ds, "keys"):
from datasets import concatenate_datasets
ds = concatenate_datasets([ds[s] for s in ds])
for item in ds:
scene_val = int(item[scene_col])
object_val = int(item[object_col])
entry = {"image": item["image"], "image_id": item["image_id"]}
cat = _classify(scene_val, object_val, relation_config)
if cat in categories and len(categories[cat]) < num_per_category:
categories[cat].append(entry)
if all(len(v) >= num_per_category for v in categories.values()):
break
for cat, imgs in categories.items():
print(f" {cat}: {len(imgs)} images")
return categories
def _classify(scene_val: int, object_val: int, rc: RelationConfig) -> str:
"""Classify an image into one of the 4 categories."""
if scene_val == 1 and object_val == 0:
return rc.scene_no_object
elif scene_val == 1 and object_val == 1:
return rc.scene_with_object
elif scene_val == 0 and object_val == 1:
return rc.non_scene_with_object
else:
return "unrelated"
def _is_adapter_dir(path: str) -> bool:
# Local directory with adapter_config.json
if os.path.exists(os.path.join(path, "adapter_config.json")):
return True
# HuggingFace Hub repo ID (e.g. "user/repo-name")
if not os.path.isabs(path) and path.count("/") == 1:
return True
return False
def _restore_adapters(model, adapter_states, device):
"""Reconstruct GRACE/WISE adapter modules from saved state."""
import copy
for module_path, saved in adapter_states.items():
parent_path, attr_name = module_path.rsplit(".", 1)
parent = model.get_submodule(parent_path)
original_layer = getattr(parent, attr_name)
extra = saved["extra"]
cfg = extra["config"]
if saved["type"] == "GRACEAdapter":
from easyeditor.models.grace.GRACE import GRACEAdapter
config = type("Cfg", (), {
"eps": cfg["eps"], "dist_fn": cfg["dist_fn"],
"replacement": cfg["replacement"],
"num_pert": cfg["num_pert"], "dropout": 0.0,
"val_init": cfg.get("val_init", "cold"),
})()
adapter = GRACEAdapter(config, original_layer, transpose=True).to(device)
adapter.keys = extra["keys"].to(device)
adapter.values = torch.nn.Parameter(
saved["state_dict"]["values"].to(device))
adapter.epsilons = extra["epsilons"].to(device)
adapter.key_labels = extra["key_labels"]
adapter.edit_ids = extra["edit_ids"]
elif saved["type"] == "WISEAdapter":
from easyeditor.models.wise.WISE import WISEAdapter
config = type("Cfg", (), {
"model_name": cfg["model_name"],
"retrieve": cfg["retrieve"],
"act_ratio": cfg["act_ratio"],
"merge_alg": cfg["merge_alg"],
"save_freq": cfg.get("save_freq"),
"densities": cfg.get("densities"),
"weights": cfg.get("weights"),
})()
adapter = WISEAdapter(config, original_layer, transpose=True).to(device)
adapter.new_weight = extra["new_weight"].to(device)
adapter.original_layer.load_state_dict(extra["original_layer_state"])
adapter.original_layer = adapter.original_layer.to(device)
adapter.memory_weight = [w.to(device) for w in extra["memory_weight"]]
adapter.memory_mean_act = extra["memory_mean_act"]
adapter.editing_mean_act = extra["editing_mean_act"]
# Restore learned parameters from state_dict
adapter.load_state_dict(saved["state_dict"], strict=False)
else:
raise ValueError(f"Unknown adapter type: {saved['type']}")
setattr(parent, attr_name, adapter)
return model
def _restore_dualedit(model, state_path: str, device: str):
"""Reconstruct DualEdit adapters from saved state and hook them to model."""
from experiment.knowledge_editing.dualedit.adapter import VisionEditAdapter, TextEditAdapter
state = torch.load(state_path, map_location=device, weights_only=False)
hp = state["hparams"]
# Create adapters
vision_adapter = VisionEditAdapter(
hidden_size=hp["hidden_size"],
mid_dim=hp["adapter_mid_dim"],
cross_att_head_n=hp["cross_att_head_n"],
img_tok_n=hp["img_tok_n"],
).to(device)
text_adapter = TextEditAdapter(
hidden_size=hp["hidden_size"],
mid_dim=hp["adapter_mid_dim"],
cross_att_head_n=hp["cross_att_head_n"],
).to(device)
# Load trained weights
vision_adapter.load_state_dict(state["vision_adapter_state"])
text_adapter.load_state_dict(state["text_adapter_state"])
# Set edit signals (mean over training set)
vision_adapter.set_edit_signal(
state["mean_vis_edit_reps"].to(device),
state["mean_vis_edit_mask"].to(device),
)
text_adapter.set_edit_signal(
state["mean_txt_edit_reps"].to(device),
state["mean_txt_edit_mask"].to(device),
)
# Set gate
vision_adapter.set_gate(state["gate_prototype"].to(device), state["gate_threshold"])
text_adapter.set_gate(state["gate_prototype"].to(device), state["gate_threshold"])
vision_adapter.open_adapter(True)
text_adapter.open_adapter(True)
vision_adapter.open_gating = True
text_adapter.open_gating = True
# Hook adapters to model layers
vision_layer_name = hp["llm_layer_tmp"].format(hp["vision_adapter_layer"])
text_layer_name = hp["llm_layer_tmp"].format(hp["text_adapter_layer"])
def _find_module(m, path):
for part in path.split("."):
m = m[int(part)] if part.isdigit() else getattr(m, part)
return m
def make_hook(adapter):
def hook(module, args, output):
if isinstance(output, tuple):
out = list(output)
out[0] = adapter(out[0])
return tuple(out)
return adapter(output)
return hook
vision_layer = _find_module(model, vision_layer_name)
text_layer = _find_module(model, text_layer_name)
vision_layer.register_forward_hook(make_hook(vision_adapter))
text_layer.register_forward_hook(make_hook(text_adapter))
# Patch model.generate to call set_input_info before each generation.
image_token_id = model.config.image_token_index
img_tok_n = hp["img_tok_n"]
_va = vision_adapter
_ta = text_adapter
_original_generate = model.generate
def _generate_with_adapter_info(*args, **kwargs):
input_ids = kwargs.get("input_ids")
if input_ids is not None and input_ids.shape[1] > 1:
positions = (input_ids[0] == image_token_id).nonzero(as_tuple=True)[0]
if len(positions) > 0:
vt_begin = int(positions[0])
vt_end = vt_begin + img_tok_n
merged_len = input_ids.shape[1] - 1 + img_tok_n
print(f" [DualEdit] set_input_info: vt_begin={vt_begin}, vt_end={vt_end}, merged_len={merged_len}, image_token_id={image_token_id}")
_va.set_input_info(True, vt_begin, vt_end)
_ta.set_input_info(True, vt_begin, vt_end)
_ta.prompt_end = torch.tensor([merged_len], device=input_ids.device)
else:
print(f" [DualEdit] WARNING: image token {image_token_id} not found in input_ids (tokens: {input_ids[0].tolist()[:10]}...)")
_va.set_input_info(False, None, None)
_ta.set_input_info(False, None, None)
else:
print(f" [DualEdit] WARNING: input_ids missing or single-token in generate kwargs")
return _original_generate(*args, **kwargs)
model.generate = _generate_with_adapter_info
# Store refs for potential later access
model._dualedit_vision_adapter = vision_adapter
model._dualedit_text_adapter = text_adapter
return model
def load_base_model(base_model_name: str, device: str):
model = AutoModelForPreTraining.from_pretrained(
base_model_name, torch_dtype=dtype,
).to(device)
model.eval()
return model
def load_finetuned_model(args, device: str):
if args.model_type == "lora":
from peft import PeftModel
model_dir = args.model_dir
if _is_adapter_dir(model_dir):
print(f" Detected LoRA adapter at {model_dir}")
base = AutoModelForPreTraining.from_pretrained(
args.base_model_name, torch_dtype=dtype,
).to(device)
model = PeftModel.from_pretrained(base, model_dir)
else:
print(" No adapter_config.json found; treating as merged model")
model = AutoModelForPreTraining.from_pretrained(
model_dir, torch_dtype=dtype,
).to(device)
model.eval()
return model
if args.model_type == "merged":
model = AutoModelForPreTraining.from_pretrained(
args.model_dir, torch_dtype=dtype,
).to(device)
model.eval()
return model
if args.model_type == "delta_w":
from model.llava.hooked_llava import HookedSAELlavaConditionalGeneration
model = HookedSAELlavaConditionalGeneration.from_pretrained(
args.base_model_name, torch_dtype=dtype,
).to(device)
state_dict = torch.load(args.checkpoint, map_location=device)
model.load_state_dict(state_dict, strict=True)
model.eval()
return model
if args.model_type in ("grace", "wise"):
model = AutoModelForPreTraining.from_pretrained(
args.base_model_name, torch_dtype=dtype,
).to(device)
adapter_path = os.path.join(args.model_dir, "adapter_state.pt")
if os.path.exists(adapter_path):
adapter_states = torch.load(adapter_path, map_location=device, weights_only=False)
model = _restore_adapters(model, adapter_states, device)
print(f" Restored {len(adapter_states)} adapter(s) from {adapter_path}")
model.eval()
return model
if args.model_type == "dualedit":
model = AutoModelForPreTraining.from_pretrained(
args.base_model_name, torch_dtype=dtype,
).to(device)
dualedit_path = os.path.join(args.model_dir, "dualedit_state.pt")
if os.path.exists(dualedit_path):
model = _restore_dualedit(model, dualedit_path, device)
print(f" Restored DualEdit adapters from {dualedit_path}")
model.eval()
return model
if args.model_type == "visedit":
visedit_dir = args.visedit_dir or str(
Path(__file__).resolve().parents[2] / "VisEdit"
)
if visedit_dir not in sys.path:
sys.path.insert(0, visedit_dir)
# Patch GLOBAL.py to point at correct root + model path
global_py = Path(visedit_dir) / "utils" / "GLOBAL.py"
global_py.write_text(
f"ROOT_PATH = {visedit_dir!r}\n"
f"model_path_map = {{\n"
f" 'llava-v1.5-7b': {args.base_model_name!r},\n"
f" 'blip2-opt-2.7b': 'models/blip2-opt-2.7b',\n"
f" 'minigpt-4-vicuna-7b': 'models/minigpt-4-vicuna-7b',\n"
f"}}\n"
)
from utils import load_vllm_editor
ckpt_path = args.model_dir
editor = load_vllm_editor(
"vead", "llava", device, extra_devices=[],
editor_ckpt_path=ckpt_path, for_train=False,
)
print(f" Loaded VEAD editor from {ckpt_path}")
return editor
raise ValueError(f"Unknown model_type: {args.model_type}")
def _load_processor_from(name_or_path: str):
"""Load processor, falling back to manual component construction on version mismatches."""
try:
return AutoProcessor.from_pretrained(name_or_path)
except Exception:
pass
try:
from transformers import AutoTokenizer, CLIPImageProcessor, LlavaProcessor
tokenizer = AutoTokenizer.from_pretrained(name_or_path, use_fast=False)
image_processor = CLIPImageProcessor.from_pretrained(name_or_path)
return LlavaProcessor(tokenizer=tokenizer, image_processor=image_processor)
except Exception as e:
raise RuntimeError(
f"Failed to load processor from {name_or_path!r}. "
"Try deleting the HuggingFace cache for this model and re-downloading."
) from e
def load_processor(args):
if args.model_type == "lora" and args.model_dir and _is_adapter_dir(args.model_dir):
return _load_processor_from(args.base_model_name)
if args.model_type in ("delta_w", "grace", "wise", "dualedit", "visedit"):
return _load_processor_from(args.base_model_name)
try:
source = args.model_dir if args.model_dir else args.base_model_name
return _load_processor_from(source)
except Exception:
return _load_processor_from(args.base_model_name)
def infer_run_name(args) -> str:
path = args.model_dir or args.checkpoint or "unknown"
parts = os.path.normpath(path).split(os.sep)
for part in reversed(parts):
if part.startswith("run_"):
return part
return os.path.basename(os.path.dirname(path)) or os.path.basename(path) or "run"
def main():
args = parse_args()
# Load relation config
relation_config = get_relation_config(args.relation)
dataset_id = args.dataset_id or relation_config.dataset_id
# Resolve train_prompts from relation config if not specified
train_prompts = args.train_prompts or relation_config.train_prompts
if args.inference_backend == "vllm":
device = "cuda"
else:
device = "cuda" if torch.cuda.is_available() else "cpu"
run_name = infer_run_name(args)
eval_dir = os.path.join(args.output_dir, run_name)
os.makedirs(eval_dir, exist_ok=True)
# Merge generality (unseen) prompts into the full prompt list for inference.
all_prompts = list(args.prompts)
if args.generality_prompts:
for p in args.generality_prompts:
if p not in all_prompts:
all_prompts.append(p)
args.prompts = all_prompts
print("=" * 70)
print("Validate Hallucination Removal")
print("=" * 70)
print(f" relation: {relation_config}")
print(f" model_type: {args.model_type}")
print(f" mention_method: {args.mention_method}")
print(f" backend: {args.inference_backend}")
print(f" device: {device}")
print(f" output_dir: {eval_dir}")
print(f" train_prompts: {train_prompts}")
print(f" all_prompts: {all_prompts}")
if args.edit_image_ids:
print(f" NOTE: --edit_image_ids is deprecated and ignored (using first N deterministically)")
print("\nLoading images by category...")
categories = load_category_images(
relation_config=relation_config,
num_per_category=args.num_per_category,
use_val_split=args.use_val_split,
dataset_id=dataset_id,
csv_path=args.val_csv,
image_dir=args.val_image_dir,
)
processor = load_processor(args)
# -- Original model outputs: load from cache or generate & save --
cache_dir = args.original_cache_dir
os.makedirs(cache_dir, exist_ok=True)
# Include relation in cache filename to avoid cross-relation collisions
all_cache_file = os.path.join(cache_dir, f"original_outputs_{args.relation}_all.json")
specific_cache_file = os.path.join(
cache_dir,
f"original_outputs_{args.relation}_n{args.num_per_category}_p{len(args.prompts)}.json",
)
if os.path.exists(all_cache_file):
print(f"\n[1/3] Loading cached original model outputs from {all_cache_file}")
with open(all_cache_file, "r") as f:
all_cache = json.load(f)
needed_ids = {
cat: {entry["image_id"] for entry in entries}
for cat, entries in categories.items()
}
needed_prompts = set(args.prompts)
original_outputs = {}
for cat, entries in all_cache.items():
original_outputs[cat] = [
e for e in entries
if e["image_id"] in needed_ids.get(cat, set())
and e["prompt"] in needed_prompts
]
elif os.path.exists(specific_cache_file):
print(f"\n[1/3] Loading cached original model outputs from {specific_cache_file}")
with open(specific_cache_file, "r") as f:
original_outputs = json.load(f)
else:
print("\n[1/3] Inference: loading original model...")
original_model = load_base_model(args.base_model_name, device)
shared_collect_orig = dict(
processor=processor,
categories=categories,
prompts=args.prompts,
max_new_tokens=args.max_new_tokens,
device=device,
)
print(" Collecting original model outputs...")
original_outputs = collect_outputs_transformers(
model=original_model, label="original", **shared_collect_orig
)
del original_model
torch.cuda.empty_cache()
with open(specific_cache_file, "w") as f:
json.dump(original_outputs, f, indent=2)
print(f" Cached original outputs to {specific_cache_file}")
# -- Fine-tuned model outputs --
shared_collect = dict(
processor=processor,
categories=categories,
prompts=args.prompts,
max_new_tokens=args.max_new_tokens,
device=device,
)
print("\n[1/3] Inference: loading fine-tuned model...")
finetuned_model = load_finetuned_model(args, device)
print(" Collecting fine-tuned model outputs...")
if args.model_type == "visedit":
edit_targets = None
if args.edit_targets and os.path.exists(args.edit_targets):
with open(args.edit_targets) as f:
edit_targets = json.load(f)
print(f" Loaded {len(edit_targets)} edit targets from {args.edit_targets}")
finetuned_outputs = collect_outputs_visedit(
editor=finetuned_model,
categories=categories,
prompts=args.prompts,
max_new_tokens=args.max_new_tokens,
edit_targets=edit_targets,
label="visedit",
relation_config=relation_config,
)
else:
finetuned_outputs = collect_outputs_transformers(
model=finetuned_model, label="finetuned", **shared_collect
)
del finetuned_model
torch.cuda.empty_cache()
print("\nLoading metrics...")
keyword_detector, clip_scorer, judge = build_metrics(
mention_method=args.mention_method,
clip_model=args.clip_model,
judge_model=args.judge_model,
judge_device=args.judge_device,
judge_max_tokens=args.judge_max_tokens,
mention_keywords=relation_config.mention_keywords,
object_name=relation_config.judge_object_name,
)
print("\n[2/3] Evaluating metrics from collected outputs...")
image_lookup = {
entry["image_id"]: entry["image"]
for cat_entries in categories.values()
for entry in cat_entries
if "image" in entry
}
original_results = evaluate_collected_outputs(
collected_outputs=original_outputs,
keyword_detector=keyword_detector,
clip_scorer=clip_scorer,
judge=judge,
mention_method=args.mention_method,
label="original",
image_lookup=image_lookup,
)
finetuned_results = evaluate_collected_outputs(
collected_outputs=finetuned_outputs,
keyword_detector=keyword_detector,
clip_scorer=clip_scorer,
judge=judge,
mention_method=args.mention_method,
label="finetuned",
image_lookup=image_lookup,
)
print("\n Computing KME metrics (locality, generality, consistency)...")
similarity_scorer = TextSimilarityScorer()
kme_metrics = compute_kme_metrics(
original_outputs=original_outputs,
finetuned_outputs=finetuned_outputs,
keyword_detector=keyword_detector,
train_prompts=train_prompts,
similarity_scorer=similarity_scorer,
efficacy_category=relation_config.efficacy_category,
locality_categories=relation_config.locality_categories,
)
print("\n[3/3] Results")
print_summary(categories, original_results, finetuned_results,
kme_metrics=kme_metrics,
relation_config=relation_config)
summary = {}
for cat in categories:
o = original_results[cat]
f = finetuned_results[cat]
summary[cat] = {
"original": {k: v for k, v in o.items() if k != "details"},
"finetuned": {k: v for k, v in f.items() if k != "details"},
"delta_clip": (
(f["avg_clip_score"] - o["avg_clip_score"])
if not (math.isnan(f["avg_clip_score"]) or math.isnan(o["avg_clip_score"]))
else None
),
}
config_snapshot = {
"relation": args.relation,
"model_type": args.model_type,
"base_model_name": args.base_model_name,
"model_dir": args.model_dir,
"checkpoint": args.checkpoint,
"val_csv": args.val_csv,
"num_per_category": args.num_per_category,
"prompts": args.prompts,
"train_prompts": train_prompts,
"generality_prompts": args.generality_prompts,
"mention_method": args.mention_method,
"clip_model": args.clip_model,
"judge_model": args.judge_model,
"judge_device": args.judge_device,
"inference_backend": args.inference_backend,
"vllm_batch_size": args.vllm_batch_size,
"vllm_tensor_parallel_size": args.vllm_tensor_parallel_size,
"vllm_gpu_memory_utilization": args.vllm_gpu_memory_utilization,
"vllm_max_model_len": args.vllm_max_model_len,
}
# Serialize KME metrics (NaN → null for JSON)
kme_serializable = {
k: (None if isinstance(v, float) and math.isnan(v) else v)
for k, v in kme_metrics.items()
}
results_path = os.path.join(eval_dir, "validation_results.json")
with open(results_path, "w") as f:
json.dump({
"summary": summary,
"kme_metrics": kme_serializable,
"config": config_snapshot,
}, f, indent=2)
details_path = os.path.join(eval_dir, "validation_details.json")
with open(details_path, "w") as f:
json.dump({
"original": {cat: r["details"] for cat, r in original_results.items()},
"finetuned": {cat: r["details"] for cat, r in finetuned_results.items()},
}, f, indent=2, default=lambda x: None if (isinstance(x, float) and math.isnan(x)) else x)
print(f"\nResults saved to: {results_path}")
print(f"Details saved to: {details_path}")
print(f"\n{'=' * 70}")
print("Validation Complete!")
print(f"{'=' * 70}")
if __name__ == "__main__":
main()
|