| """ |
| Evaluate hallucination of a LoRA-trained LLaVA model on the bathroom/toilet dataset. |
| |
| Mirrors build_caption_targets.py but loads the trained model (base + LoRA adapter) |
| instead of the original model. |
| |
| Two-stage pipeline: |
| Stage 1: Run LoRA-trained LLaVA on all images -> raw captions |
| Stage 1.5: Regex coarse filter + LLM judge to confirm toilet mentions |
| |
| Hallucinating = image has no toilet (ground truth) but the LoRA model mentions toilet. |
| |
| Optionally loads the original caption_targets.json to produce a before/after comparison. |
| |
| Usage: |
| # Full pipeline (inference + LLM judge) |
| python -m experiment.data.eval_lora_hallucination \\ |
| --lora_dir step3_lora_v5_outputs/run_20260317_000000/lora_adapter \\ |
| --output experiment/data/lora_hallucination_results.json |
| |
| # With comparison against original model captions |
| python -m experiment.data.eval_lora_hallucination \\ |
| --lora_dir step3_lora_v5_outputs/run_20260317_000000/lora_adapter \\ |
| --original_targets experiment/data/caption_targets.json \\ |
| --output experiment/data/lora_hallucination_results.json |
| |
| # Inference only (no LLM judge) |
| python -m experiment.data.eval_lora_hallucination \\ |
| --lora_dir step3_lora_v5_outputs/run_20260317_000000/lora_adapter \\ |
| --inference_only |
| |
| # Run LLM judge on an existing result file |
| python -m experiment.data.eval_lora_hallucination \\ |
| --judge_only experiment/data/lora_hallucination_results.json |
| |
| Output format (lora_hallucination_results.json): |
| { |
| "images": { |
| "<image_id>": { |
| "bathroom": 1, |
| "toilet": 0, |
| "split": "train", |
| "category": "bathroom_no_toilet", |
| "lora_caption": "A bathroom with a sink and a large mirror.", |
| "had_toilet_mention_regex": false, |
| "had_toilet_mention_llm": false, |
| "is_hallucinating": false, |
| "original_caption": "A bathroom with a toilet, sink...", // if --original_targets provided |
| "was_hallucinating_before": true // if --original_targets provided |
| }, |
| ... |
| }, |
| "stats": { |
| "total_images": 500, |
| "with_captions": 500, |
| "had_toilet_mention_regex": 12, |
| "had_toilet_mention_llm": 10, |
| "hallucinating": 10, |
| "hallucination_rate": 0.02, |
| "by_category": { ... }, |
| "comparison": { // only present when --original_targets provided |
| "original_hallucinating": 80, |
| "lora_hallucinating": 10, |
| "delta": -70, |
| "suppression_rate": 0.875 |
| } |
| }, |
| "config": { ... } |
| } |
| """ |
|
|
| import argparse |
| import json |
| import os |
| import re |
| import sys |
| from typing import Optional |
|
|
| from tqdm import tqdm |
|
|
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../..")) |
|
|
| from experiment.data.hf_loader import HF_DATASET_ID, hf_rows as _hf_rows |
| from experiment.data.build_caption_targets import ( |
| load_csv, |
| judge_hallucination_with_llm, |
| TOILET_KEYWORDS, |
| _TOILET_RE, |
| _JUDGE_PROMPT, |
| CAPTION_PROMPT, |
| ) |
|
|
| |
| |
| |
|
|
| def _worker_inference_lora( |
| gpu_id: str, |
| rank: int, |
| rows: list[dict], |
| base_model_name: str, |
| lora_dir: str, |
| prompt_text: str, |
| batch_size: int, |
| return_dict: dict, |
| ): |
| """Single-GPU worker: loads base model + LoRA adapter, runs inference.""" |
| import os |
| os.environ["CUDA_VISIBLE_DEVICES"] = gpu_id |
|
|
| import torch |
| from PIL import Image |
| from transformers import AutoProcessor, LlavaForConditionalGeneration |
| from peft import PeftModel |
|
|
| processor = AutoProcessor.from_pretrained(base_model_name) |
| base = LlavaForConditionalGeneration.from_pretrained( |
| base_model_name, torch_dtype=torch.float16, device_map="cuda", |
| ) |
| model = PeftModel.from_pretrained(base, lora_dir) |
| model.eval() |
|
|
| valid_rows = [] |
| images = [] |
| for row in rows: |
| try: |
| if "image_path" in row: |
| image = Image.open(row["image_path"]).convert("RGB") |
| else: |
| image = row["image"].convert("RGB") |
| valid_rows.append(row) |
| images.append(image) |
| except Exception as e: |
| print(f" [GPU {rank}] Skipping {row['image_id']}: {e}") |
|
|
| results = {} |
| for i in tqdm(range(0, len(valid_rows), batch_size), |
| desc=f"LoRA Inference (GPU {rank})", position=rank): |
| batch_rows = valid_rows[i:i + batch_size] |
| batch_images = images[i:i + batch_size] |
|
|
| inputs = processor( |
| text=[prompt_text] * len(batch_images), |
| images=batch_images, |
| return_tensors="pt", |
| padding=True, |
| ).to("cuda") |
|
|
| with torch.no_grad(): |
| output_ids = model.generate( |
| **inputs, |
| max_new_tokens=300, |
| do_sample=False, |
| ) |
|
|
| input_len = inputs["input_ids"].shape[1] |
| for row, out_ids in zip(batch_rows, output_ids): |
| generated = processor.decode( |
| out_ids[input_len:], skip_special_tokens=True, |
| ).strip() |
| results[row["image_id"]] = { |
| "lora_caption": generated, |
| "had_toilet_mention": bool(_TOILET_RE.search(generated)), |
| } |
|
|
| del model |
| torch.cuda.empty_cache() |
| return_dict[rank] = results |
|
|
|
|
| def run_lora_inference( |
| rows: list[dict], |
| base_model_name: str, |
| lora_dir: str, |
| prompt: str, |
| categories: Optional[list[str]] = None, |
| batch_size: int = 8, |
| num_gpus: int = 1, |
| ) -> dict[str, dict]: |
| """Run LoRA-trained LLaVA inference with data parallelism. |
| |
| Args: |
| rows: list of row dicts from load_csv() |
| base_model_name: HuggingFace base model ID |
| lora_dir: path to LoRA adapter directory (must contain adapter_config.json) |
| prompt: captioning prompt |
| categories: which categories to caption (default: all) |
| batch_size: batch size per GPU |
| num_gpus: number of GPUs for data parallelism |
| |
| Returns: |
| dict mapping image_id → {lora_caption, had_toilet_mention} |
| """ |
| if categories: |
| rows = [r for r in rows if r["category"] in categories] |
|
|
| print(f"\nRunning LoRA inference on {len(rows)} images") |
| print(f" base model: {base_model_name}") |
| print(f" lora dir: {lora_dir}") |
| print(f" GPUs: {num_gpus}") |
|
|
| |
| _is_hub_id = not os.path.isabs(lora_dir) and lora_dir.count("/") == 1 |
| if not _is_hub_id and not os.path.exists(os.path.join(lora_dir, "adapter_config.json")): |
| raise FileNotFoundError( |
| f"No adapter_config.json found in {lora_dir!r}. " |
| "Make sure --lora_dir points to a trained LoRA adapter directory or a HuggingFace Hub repo ID." |
| ) |
|
|
| prompt_text = f"USER: <image>\n{prompt}\nASSISTANT:" |
|
|
| |
| visible = os.environ.get("CUDA_VISIBLE_DEVICES", "") |
| if visible: |
| gpu_ids = [g.strip() for g in visible.split(",")] |
| else: |
| import torch |
| gpu_ids = [str(i) for i in range(torch.cuda.device_count())] |
| gpu_ids = gpu_ids[:num_gpus] |
| if len(gpu_ids) < num_gpus: |
| print(f" WARNING: requested {num_gpus} GPUs but only " |
| f"{len(gpu_ids)} visible, using {len(gpu_ids)}") |
| num_gpus = len(gpu_ids) |
|
|
| |
| tmp_dir = None |
| if num_gpus > 1: |
| import tempfile |
| needs_save = any("image_path" not in r for r in rows) |
| if needs_save: |
| tmp_dir = tempfile.mkdtemp(prefix="lora_eval_") |
| print(f" Saving HF images to {tmp_dir} for multi-GPU...") |
| for row in rows: |
| if "image_path" not in row: |
| path = os.path.join(tmp_dir, f"{row['image_id']}.jpg") |
| row["image"].convert("RGB").save(path) |
| row["image_path"] = path |
| serializable_rows = [ |
| {k: v for k, v in r.items() if k != "image"} for r in rows |
| ] |
| else: |
| serializable_rows = rows |
|
|
| if num_gpus <= 1: |
| return_dict = {} |
| _worker_inference_lora( |
| gpu_id=gpu_ids[0], rank=0, rows=serializable_rows, |
| base_model_name=base_model_name, lora_dir=lora_dir, |
| prompt_text=prompt_text, batch_size=batch_size, |
| return_dict=return_dict, |
| ) |
| results = return_dict[0] |
| else: |
| import torch.multiprocessing as mp |
| mp.set_start_method("spawn", force=True) |
|
|
| shards = [[] for _ in range(num_gpus)] |
| for i, row in enumerate(serializable_rows): |
| shards[i % num_gpus].append(row) |
|
|
| manager = mp.Manager() |
| return_dict = manager.dict() |
| processes = [] |
| for rank in range(num_gpus): |
| p = mp.Process( |
| target=_worker_inference_lora, |
| args=(gpu_ids[rank], rank, shards[rank], |
| base_model_name, lora_dir, |
| prompt_text, batch_size, return_dict), |
| ) |
| p.start() |
| processes.append(p) |
|
|
| for p in processes: |
| p.join() |
|
|
| for rank, p in enumerate(processes): |
| if p.exitcode != 0: |
| raise RuntimeError( |
| f"Worker on GPU {gpu_ids[rank]} exited with code {p.exitcode}" |
| ) |
|
|
| results = {} |
| for rank in range(num_gpus): |
| results.update(return_dict[rank]) |
|
|
| if tmp_dir is not None: |
| import shutil |
| shutil.rmtree(tmp_dir, ignore_errors=True) |
| for row in rows: |
| if row.get("image_path", "").startswith(tmp_dir): |
| del row["image_path"] |
|
|
| n_toilet = sum(1 for r in results.values() if r["had_toilet_mention"]) |
| print(f" {len(results)} captions generated") |
| print(f" {n_toilet}/{len(results)} mentioned toilet (regex)") |
|
|
| return results |
|
|
|
|
| |
| |
| |
|
|
| def _worker_judge( |
| gpu_id: str, |
| rank: int, |
| image_ids: list, |
| captions: dict, |
| model_name: str, |
| batch_size: int, |
| gpu_memory_utilization: float, |
| return_dict: dict, |
| ): |
| """Single-GPU worker: loads one full judge model copy, processes a shard.""" |
| import os |
| os.environ["CUDA_VISIBLE_DEVICES"] = gpu_id |
|
|
| import torch |
| from vllm import LLM, SamplingParams |
| from transformers import AutoTokenizer |
|
|
| tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) |
|
|
| prompts = [] |
| for iid in image_ids: |
| user_msg = _JUDGE_PROMPT.format(caption=captions[iid].replace('"', "'")) |
| messages = [{"role": "user", "content": user_msg}] |
| text = tokenizer.apply_chat_template( |
| messages, tokenize=False, add_generation_prompt=True, |
| enable_thinking=False, |
| ) |
| prompts.append(text) |
|
|
| llm = LLM( |
| model=model_name, |
| trust_remote_code=True, |
| gpu_memory_utilization=gpu_memory_utilization, |
| tensor_parallel_size=1, |
| dtype="float16", |
| ) |
| outputs = llm.generate(prompts, SamplingParams(max_tokens=10, temperature=0)) |
|
|
| results = {} |
| for iid, output in zip(image_ids, outputs): |
| response = output.outputs[0].text.strip().upper() |
| results[iid] = response.startswith("YES") |
|
|
| del llm |
| torch.cuda.empty_cache() |
| return_dict[rank] = results |
|
|
|
|
| def run_judge_ddp( |
| captions: dict, |
| model_name: str, |
| batch_size: int = 64, |
| gpu_memory_utilization: float = 0.85, |
| num_gpus: int = 1, |
| ) -> dict: |
| """Data-parallel judge: one full model copy per GPU, sharded captions. |
| |
| Falls back to the single-process judge_hallucination_with_llm when num_gpus=1. |
| """ |
| if not captions: |
| return {} |
|
|
| if num_gpus <= 1: |
| return judge_hallucination_with_llm( |
| captions, |
| model_name=model_name, |
| batch_size=batch_size, |
| gpu_memory_utilization=gpu_memory_utilization, |
| tensor_parallel_size=1, |
| ) |
|
|
| print(f"\nJudging {len(captions)} captions with {model_name} " |
| f"(DDP, {num_gpus} GPUs)...") |
|
|
| visible = os.environ.get("CUDA_VISIBLE_DEVICES", "") |
| if visible: |
| gpu_ids = [g.strip() for g in visible.split(",")] |
| else: |
| import torch |
| gpu_ids = [str(i) for i in range(torch.cuda.device_count())] |
| gpu_ids = gpu_ids[:num_gpus] |
| if len(gpu_ids) < num_gpus: |
| print(f" WARNING: requested {num_gpus} GPUs but only " |
| f"{len(gpu_ids)} visible, using {len(gpu_ids)}") |
| num_gpus = len(gpu_ids) |
|
|
| iids = list(captions.keys()) |
| shards = [[] for _ in range(num_gpus)] |
| for i, iid in enumerate(iids): |
| shards[i % num_gpus].append(iid) |
|
|
| import torch.multiprocessing as mp |
| try: |
| mp.set_start_method("spawn", force=True) |
| except RuntimeError: |
| pass |
|
|
| manager = mp.Manager() |
| return_dict = manager.dict() |
| processes = [] |
| for rank in range(num_gpus): |
| p = mp.Process( |
| target=_worker_judge, |
| args=(gpu_ids[rank], rank, shards[rank], captions, |
| model_name, batch_size, gpu_memory_utilization, |
| return_dict), |
| ) |
| p.start() |
| processes.append(p) |
|
|
| for p in processes: |
| p.join() |
|
|
| for rank, p in enumerate(processes): |
| if p.exitcode != 0: |
| raise RuntimeError( |
| f"Judge worker on GPU {gpu_ids[rank]} exited with code {p.exitcode}" |
| ) |
|
|
| results = {} |
| for rank in range(num_gpus): |
| results.update(return_dict[rank]) |
|
|
| n_confirmed = sum(1 for v in results.values() if v) |
| print(f" LLM confirmed {n_confirmed}/{len(results)} as mentioning toilet") |
| print(f" Regex false positives filtered: {len(results) - n_confirmed}") |
|
|
| return results |
|
|
|
|
| |
| |
| |
|
|
| def build_results( |
| rows: list[dict], |
| inference_results: dict[str, dict], |
| judge_results: Optional[dict[str, bool]] = None, |
| original_targets: Optional[dict] = None, |
| ) -> dict: |
| """Build the lora_hallucination_results.json structure. |
| |
| Args: |
| rows: all dataset rows (from load_csv) |
| inference_results: output of run_lora_inference |
| judge_results: output of judge_hallucination_with_llm (optional) |
| original_targets: loaded caption_targets.json for comparison (optional) |
| """ |
| images = {} |
| orig_images = (original_targets or {}).get("images", {}) |
|
|
| for row in rows: |
| iid = row["image_id"] |
| entry = { |
| "bathroom": row["bathroom"], |
| "toilet": row["toilet"], |
| "split": row["split"], |
| "category": row["category"], |
| "lora_caption": None, |
| "had_toilet_mention_regex": None, |
| "had_toilet_mention_llm": None, |
| "is_hallucinating": None, |
| } |
|
|
| if iid in inference_results: |
| inf = inference_results[iid] |
| entry["lora_caption"] = inf["lora_caption"] |
| entry["had_toilet_mention_regex"] = inf["had_toilet_mention"] |
|
|
| |
| if judge_results is not None: |
| if iid in judge_results: |
| entry["had_toilet_mention_llm"] = judge_results[iid] |
| elif entry["had_toilet_mention_regex"] is False: |
| entry["had_toilet_mention_llm"] = False |
|
|
| |
| if entry["had_toilet_mention_llm"] is not None: |
| entry["is_hallucinating"] = ( |
| row["toilet"] == 0 and entry["had_toilet_mention_llm"] |
| ) |
|
|
| |
| if iid in orig_images: |
| orig = orig_images[iid] |
| entry["original_caption"] = orig.get("original_caption") |
| entry["was_hallucinating_before"] = orig.get("is_hallucinating") |
|
|
| images[iid] = entry |
|
|
| |
| all_entries = list(images.values()) |
| n_total = len(all_entries) |
| n_with_caption = sum(1 for e in all_entries if e["lora_caption"]) |
| n_regex = sum(1 for e in all_entries if e.get("had_toilet_mention_regex")) |
| n_llm = sum(1 for e in all_entries if e.get("had_toilet_mention_llm")) |
| n_hallucinating = sum(1 for e in all_entries if e.get("is_hallucinating")) |
|
|
| from collections import Counter |
| by_category = {} |
| for cat in ["bathroom_no_toilet", "bathroom_with_toilet", |
| "non_bathroom_with_toilet", "unrelated"]: |
| cat_entries = [e for e in all_entries if e["category"] == cat] |
| n_cat = len(cat_entries) |
| n_cat_hal = sum(1 for e in cat_entries if e.get("is_hallucinating")) |
| by_category[cat] = { |
| "total": n_cat, |
| "hallucinating": n_cat_hal, |
| "hallucination_rate": round(n_cat_hal / n_cat, 4) if n_cat > 0 else 0.0, |
| } |
|
|
| stats = { |
| "total_images": n_total, |
| "with_captions": n_with_caption, |
| "had_toilet_mention_regex": n_regex, |
| "had_toilet_mention_llm": n_llm, |
| "hallucinating": n_hallucinating, |
| "hallucination_rate": round(n_hallucinating / n_with_caption, 4) if n_with_caption > 0 else 0.0, |
| "by_category": by_category, |
| "by_split": dict(Counter(e["split"] for e in all_entries)), |
| } |
|
|
| |
| if orig_images: |
| n_orig_hal = sum( |
| 1 for e in all_entries |
| if e.get("was_hallucinating_before") |
| ) |
| n_lora_hal = n_hallucinating |
| delta = n_lora_hal - n_orig_hal |
| suppression = ( |
| round((n_orig_hal - n_lora_hal) / n_orig_hal, 4) |
| if n_orig_hal > 0 else 0.0 |
| ) |
| stats["comparison"] = { |
| "original_hallucinating": n_orig_hal, |
| "lora_hallucinating": n_lora_hal, |
| "delta": delta, |
| "suppression_rate": suppression, |
| } |
|
|
| return {"images": images, "stats": stats} |
|
|
|
|
| def save_results(results: dict, output_path: str, config: dict): |
| results["config"] = config |
| os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) |
| with open(output_path, "w") as f: |
| json.dump(results, f, indent=2) |
| print(f"\nSaved to {output_path}") |
| stats = results["stats"] |
| print(f" Total images: {stats['total_images']}") |
| print(f" Hallucinating (regex): {stats['had_toilet_mention_regex']}") |
| print(f" Hallucinating (LLM): {stats['hallucinating']}") |
| print(f" Hallucination rate: {stats['hallucination_rate']:.2%}") |
| if "comparison" in stats: |
| c = stats["comparison"] |
| print(f"\n === Before / After Comparison ===") |
| print(f" Original hallucinating: {c['original_hallucinating']}") |
| print(f" LoRA hallucinating: {c['lora_hallucinating']}") |
| print(f" Delta: {c['delta']:+d}") |
| print(f" Suppression rate: {c['suppression_rate']:.2%}") |
| print(f"\n By category:") |
| for cat, d in stats["by_category"].items(): |
| print(f" {cat}: {d['hallucinating']}/{d['total']} " |
| f"({d['hallucination_rate']:.2%})") |
|
|
|
|
| |
| |
| |
|
|
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Evaluate hallucination of a LoRA-trained LLaVA on the bathroom/toilet dataset" |
| ) |
|
|
| |
| parser.add_argument("--csv", type=str, default=None, |
| help="(Legacy) Path to CSV. If omitted, loads from HuggingFace.") |
| parser.add_argument("--image_dir", type=str, default=None, |
| help="(Legacy) Image directory.") |
| parser.add_argument("--dataset_id", type=str, default=HF_DATASET_ID, |
| help="HuggingFace dataset ID") |
| parser.add_argument("--output", type=str, |
| default="experiment/data/lora_hallucination_results.json") |
|
|
| |
| parser.add_argument("--base_model", type=str, default="llava-hf/llava-1.5-7b-hf", |
| help="Base LLaVA model name (HuggingFace ID)") |
| parser.add_argument("--lora_dir", type=str, default=None, |
| help="Path to LoRA adapter directory (must contain adapter_config.json)") |
| parser.add_argument("--judge_model", type=str, default="Qwen/Qwen3-8B", |
| help="LLM for judging toilet mentions (via vLLM)") |
| parser.add_argument("--prompt", type=str, default=CAPTION_PROMPT) |
| parser.add_argument("--batch_size", type=int, default=8, |
| help="Inference batch size per GPU") |
| parser.add_argument("--num_gpus", type=int, default=1, |
| help="Number of GPUs for data-parallel inference") |
| parser.add_argument("--judge_batch_size", type=int, default=64) |
| parser.add_argument("--judge_gpu_memory", type=float, default=0.85) |
| parser.add_argument("--judge_num_gpus", type=int, default=1, |
| help="Number of GPUs for data-parallel judge (one model copy per GPU)") |
|
|
| |
| parser.add_argument("--categories", nargs="+", |
| default=["bathroom_no_toilet", "bathroom_with_toilet", |
| "non_bathroom_with_toilet", "unrelated"], |
| help="Which categories to run inference on") |
|
|
| |
| parser.add_argument("--original_targets", type=str, default=None, |
| help="Path to caption_targets.json from the original model " |
| "(enables before/after hallucination comparison)") |
|
|
| |
| parser.add_argument("--inference_only", action="store_true", |
| help="Run inference only, skip LLM judge") |
| parser.add_argument("--skip_judge", action="store_true", |
| help="Use regex only for toilet detection (no LLM judge)") |
| parser.add_argument("--judge_only", type=str, default=None, |
| help="Path to existing lora_hallucination_results.json — " |
| "run LLM judge on regex-positive entries only") |
|
|
| args = parser.parse_args() |
|
|
| |
| if args.judge_only: |
| print(f"Loading existing results from {args.judge_only}") |
| with open(args.judge_only) as f: |
| results = json.load(f) |
|
|
| regex_positive = { |
| iid: entry["lora_caption"] |
| for iid, entry in results["images"].items() |
| if entry.get("lora_caption") |
| and entry.get("had_toilet_mention_regex") |
| and entry.get("had_toilet_mention_llm") is None |
| } |
|
|
| if not regex_positive: |
| print("All regex-positive entries already judged.") |
| return |
|
|
| judge_results = run_judge_ddp( |
| regex_positive, |
| model_name=args.judge_model, |
| batch_size=args.judge_batch_size, |
| gpu_memory_utilization=args.judge_gpu_memory, |
| num_gpus=args.judge_num_gpus, |
| ) |
|
|
| for iid, confirmed in judge_results.items(): |
| entry = results["images"][iid] |
| entry["had_toilet_mention_llm"] = confirmed |
| entry["is_hallucinating"] = ( |
| entry.get("toilet", 0) == 0 and confirmed |
| ) |
|
|
| |
| for iid, entry in results["images"].items(): |
| if entry.get("had_toilet_mention_llm") is None: |
| entry["had_toilet_mention_llm"] = False |
| entry["is_hallucinating"] = False |
|
|
| |
| all_entries = list(results["images"].values()) |
| results["stats"]["had_toilet_mention_llm"] = sum( |
| 1 for e in all_entries if e.get("had_toilet_mention_llm")) |
| results["stats"]["hallucinating"] = sum( |
| 1 for e in all_entries if e.get("is_hallucinating")) |
| n_with_caption = results["stats"].get("with_captions", len(all_entries)) |
| n_hal = results["stats"]["hallucinating"] |
| results["stats"]["hallucination_rate"] = ( |
| round(n_hal / n_with_caption, 4) if n_with_caption > 0 else 0.0 |
| ) |
| |
| for cat, d in results["stats"].get("by_category", {}).items(): |
| cat_entries = [e for e in all_entries if e["category"] == cat] |
| n_cat = len(cat_entries) |
| n_cat_hal = sum(1 for e in cat_entries if e.get("is_hallucinating")) |
| d["hallucinating"] = n_cat_hal |
| d["hallucination_rate"] = round(n_cat_hal / n_cat, 4) if n_cat > 0 else 0.0 |
|
|
| save_results(results, args.judge_only, results.get("config", {})) |
| return |
|
|
| |
| if args.lora_dir is None: |
| parser.error("--lora_dir is required (unless using --judge_only)") |
|
|
| rows = load_csv(args.csv, args.image_dir, args.dataset_id) |
|
|
| original_targets = None |
| if args.original_targets: |
| print(f"\nLoading original model targets from {args.original_targets}") |
| with open(args.original_targets) as f: |
| original_targets = json.load(f) |
| print(f" {len(original_targets.get('images', {}))} entries loaded") |
|
|
| config = { |
| "base_model": args.base_model, |
| "lora_dir": args.lora_dir, |
| "judge_model": args.judge_model if not (args.inference_only or args.skip_judge) else None, |
| "prompt": args.prompt, |
| "categories": args.categories, |
| "original_targets": args.original_targets, |
| } |
|
|
| |
| inference_results = run_lora_inference( |
| rows, |
| base_model_name=args.base_model, |
| lora_dir=args.lora_dir, |
| prompt=args.prompt, |
| categories=args.categories, |
| batch_size=args.batch_size, |
| num_gpus=args.num_gpus, |
| ) |
|
|
| |
| results = build_results(rows, inference_results, |
| original_targets=original_targets) |
| save_results(results, args.output, config) |
| print("Stage 1 complete — all LoRA captions saved.") |
|
|
| if args.inference_only: |
| return |
|
|
| |
| judge_results = None |
| if not args.skip_judge: |
| regex_positive = { |
| iid: inf["lora_caption"] |
| for iid, inf in inference_results.items() |
| if inf["had_toilet_mention"] |
| } |
|
|
| if regex_positive: |
| judge_results = run_judge_ddp( |
| regex_positive, |
| model_name=args.judge_model, |
| batch_size=args.judge_batch_size, |
| gpu_memory_utilization=args.judge_gpu_memory, |
| num_gpus=args.judge_num_gpus, |
| ) |
| else: |
| print("\nNo regex-positive captions — skipping LLM judge.") |
|
|
| results = build_results(rows, inference_results, |
| judge_results=judge_results, |
| original_targets=original_targets) |
| save_results(results, args.output, config) |
| print("Stage 1.5 complete — LLM judge results saved.") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|