hallucination / EFUF /scripts /format_bathroom_data.py
ToiTenBao's picture
Upload hallucination folder
a2ffd07 verified
Raw
History Blame Contribute Delete
12.7 kB
#!/usr/bin/env python3
"""
Convert bathroom_toilet/all_captions.json + checkpoint data into EFUF training format.
Produces:
- pos_neg.json: Per-object/subsentence entries for positive and negative training
- sentences.json: Whole-caption positive reinforcement entries
- dummy_vqa.json: Minimal VQA data for EFUF DataLoader compatibility
Usage:
python scripts/format_bathroom_data.py \
--all_captions data/bathroom_toilet/all_captions.json \
--ckpt_json data/bathroom_toilet/all_captions.json.ckpt.json \
--visedit_ckpt ../VisEdit/data/hallucination/bathroom_toilet/checkpoint.json \
--image_dir ../VisEdit/data/hallucination/bathroom_toilet/images \
--output_dir data/bathroom_toilet \
--mode synthetic
"""
from __future__ import annotations
import argparse
import json
import os
import re
import shutil
from pathlib import Path
SUBSENTENCE_SPLITTER = ",.;!?:"
TARGET_WORDS = ["toilet", "toilets"]
NEGATIVE_SCORE = 0.0
POSITIVE_SCORE = 40.0
SENTENCE_MEAN_POS = 40.0
SENTENCE_MIN_POS = 35.0
SENTENCE_MEAN_NEG = 10.0
SENTENCE_MIN_NEG = 5.0
def find_subsentence(text: str, target_words: list[str]) -> tuple[int, int] | None:
"""Find the subsentence containing any of the target words.
Returns (start, end) character indices of the subsentence within text,
or None if not found.
"""
sub_sentences = re.split(f"[{SUBSENTENCE_SPLITTER}]+", text)
for sub in sub_sentences:
for word in target_words:
pattern = r"\b" + re.escape(word) + r"\b"
if re.search(pattern, sub, re.IGNORECASE):
start = text.find(sub)
end = start + len(sub)
return start, end
return None
def is_valid_toilet_mention(caption: str) -> bool:
"""Check if 'toilet' (or 'toilets') appears as a whole word, not as part
of 'toiletries' etc."""
lower = caption.lower()
for word in TARGET_WORDS:
pattern = r"\b" + re.escape(word) + r"\b"
if re.search(pattern, lower):
return True
return False
def main():
parser = argparse.ArgumentParser(description="Format bathroom_toilet data for EFUF training")
parser.add_argument("--all_captions", default="data/bathroom_toilet/all_captions.json")
parser.add_argument("--ckpt_json", default="data/bathroom_toilet/all_captions.json.ckpt.json")
parser.add_argument(
"--visedit_ckpt", default="../VisEdit/data/hallucination/bathroom_toilet/checkpoint.json"
)
parser.add_argument(
"--image_dir", default="../VisEdit/data/hallucination/bathroom_toilet/images"
)
parser.add_argument("--output_dir", default="data/bathroom_toilet")
parser.add_argument(
"--mode",
choices=["synthetic", "clip"],
default="synthetic",
help="synthetic: use fixed scores; clip: compute CLIP scores (not implemented yet)",
)
parser.add_argument("--unlearn_target", choices=["subsentence", "object"], default="subsentence")
parser.add_argument("--symlink_dir", default=None, help="Create merged image directory with symlinks")
args = parser.parse_args()
with open(args.all_captions) as f:
all_captions = json.load(f)
with open(args.ckpt_json) as f:
step_data = json.load(f)
with open(args.visedit_ckpt) as f:
visedit_ckpt = json.load(f)
step2_judge = step_data.get("step2_judge_done", {})
step3_rewrite = step_data.get("step3_rewrite_done", {})
step4_strict = step_data.get("step4_strict_done", {})
train_ids = set(visedit_ckpt["train_ids"])
val_ids = set(visedit_ckpt["val_ids"])
train_pos_ids = set(visedit_ckpt.get("train_pos_ids", []))
val_pos_ids = set(visedit_ckpt.get("val_pos_ids", []))
id_map = {item["image_id"]: item for item in all_captions}
pos_neg_entries = []
sentence_entries = []
skipped_no_toilet = 0
skipped_no_subsentence = 0
skipped_no_image = 0
train_img_dir = os.path.join(args.image_dir, "train")
val_img_dir = os.path.join(args.image_dir, "val")
def get_image_path(image_id: str) -> str | None:
for d in [train_img_dir, val_img_dir]:
p = os.path.join(d, f"{image_id}.jpg")
if os.path.exists(p):
return p
return None
def get_image_relpath(image_id: str) -> str:
"""Return relative path from image_dir for the image."""
if image_id in train_ids or image_id in train_pos_ids:
return f"train/{image_id}.jpg"
elif image_id in val_ids or image_id in val_pos_ids:
return f"val/{image_id}.jpg"
else:
for d in ["train", "val"]:
if os.path.exists(os.path.join(args.image_dir, d, f"{image_id}.jpg")):
return f"{d}/{image_id}.jpg"
return f"train/{image_id}.jpg"
for item in all_captions:
image_id = item["image_id"]
caption = item["llava_caption"]
toilet = item["toilet"]
bathroom = item["bathroom"]
if not is_valid_toilet_mention(caption):
if toilet == 0 and bathroom == 1:
sentence_entries.append(
{
"image": get_image_relpath(image_id),
"sentence": caption,
"mean": SENTENCE_MEAN_POS,
"min": SENTENCE_MIN_POS,
}
)
elif toilet == 1 and bathroom == 1:
sentence_entries.append(
{
"image": get_image_relpath(image_id),
"sentence": caption,
"mean": SENTENCE_MEAN_POS,
"min": SENTENCE_MIN_POS,
}
)
continue
if not get_image_path(image_id):
skipped_no_image += 1
continue
if toilet == 0:
# Check if this is a confirmed hallucination
judgment = step2_judge.get(image_id)
if judgment != "YES":
skipped_no_toilet += 1
continue
result = find_subsentence(caption, TARGET_WORDS)
if result is None:
skipped_no_subsentence += 1
continue
start, end = result
if args.unlearn_target == "subsentence":
sentence = caption[:end].rstrip()
position = start
while position > 0 and sentence[position - 1] == " ":
position -= 1
if position == 0:
position = start
else:
word_match = re.search(r"\b(toilet|toilets)\b", caption, re.IGNORECASE)
if word_match is None:
skipped_no_subsentence += 1
continue
start_pos = word_match.start()
end_pos = word_match.end()
sentence = caption[:end_pos]
position = start_pos
pos_neg_entries.append(
{
"image": get_image_relpath(image_id),
"sentence": sentence,
"position": position,
"score": NEGATIVE_SCORE,
"type": "negative",
}
)
elif toilet == 1:
result = find_subsentence(caption, TARGET_WORDS)
if result is None:
skipped_no_subsentence += 1
continue
start, end = result
if args.unlearn_target == "subsentence":
sentence = caption[:end].rstrip()
position = start
while position > 0 and sentence[position - 1] == " ":
position -= 1
if position == 0:
position = start
else:
word_match = re.search(r"\b(toilet|toilets)\b", caption, re.IGNORECASE)
if word_match is None:
skipped_no_subsentence += 1
continue
start_pos = word_match.start()
end_pos = word_match.end()
sentence = caption[:end_pos]
position = start_pos
pos_neg_entries.append(
{
"image": get_image_relpath(image_id),
"sentence": sentence,
"position": position,
"score": POSITIVE_SCORE,
"type": "positive",
}
)
sentence_entries.append(
{
"image": get_image_relpath(image_id),
"sentence": caption,
"mean": SENTENCE_MEAN_POS,
"min": SENTENCE_MIN_POS,
}
)
neg_count = sum(1 for e in pos_neg_entries if e["type"] == "negative")
pos_count = sum(1 for e in pos_neg_entries if e["type"] == "positive")
print(f"pos_neg.json: {len(pos_neg_entries)} entries ({neg_count} negative, {pos_count} positive)")
print(f"sentences.json: {len(sentence_entries)} entries")
print(f"Skipped: no_toilet_mention={skipped_no_toilet}, no_subsentence={skipped_no_subsentence}, no_image={skipped_no_image}")
final_pos_neg = [
{k: v for k, v in e.items() if k != "type"} for e in pos_neg_entries
]
all_ids = train_ids | val_ids | train_pos_ids | val_pos_ids
def split(entries):
train_entries = [e for e in entries if e["image"].split("/")[0] == "train"]
val_entries = [e for e in entries if e["image"].split("/")[0] == "val"]
return train_entries, val_entries
train_pos_neg, val_pos_neg = split(final_pos_neg)
train_sentences, val_sentences = split(sentence_entries)
print(f"Train pos_neg: {len(train_pos_neg)}, Val pos_neg: {len(val_pos_neg)}")
train_neg = sum(1 for e in train_pos_neg if e["score"] == NEGATIVE_SCORE)
train_pos = sum(1 for e in train_pos_neg if e["score"] == POSITIVE_SCORE)
val_neg = sum(1 for e in val_pos_neg if e["score"] == NEGATIVE_SCORE)
val_pos = sum(1 for e in val_pos_neg if e["score"] == POSITIVE_SCORE)
print(f" Train: {train_pos} positive, {train_neg} negative")
print(f" Val: {val_pos} positive, {val_neg} negative")
print(f"Train sentences: {len(train_sentences)}, Val sentences: {len(val_sentences)}")
os.makedirs(args.output_dir, exist_ok=True)
for split_name, data in [("train", train_pos_neg), ("val", val_pos_neg)]:
path = os.path.join(args.output_dir, f"pos_neg_{args.mode}_{split_name}.json")
with open(path, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f"Saved {path} ({len(data)} entries)")
for split_name, data in [("train", train_sentences), ("val", val_sentences)]:
path = os.path.join(args.output_dir, f"sentences_{args.mode}_{split_name}.json")
with open(path, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f"Saved {path} ({len(data)} entries)")
pos_neg_path = os.path.join(args.output_dir, f"pos_neg_{args.mode}.json")
with open(pos_neg_path, "w") as f:
json.dump(final_pos_neg, f, indent=2, ensure_ascii=False)
print(f"Saved {pos_neg_path}")
sentences_path = os.path.join(args.output_dir, f"sentences_{args.mode}.json")
with open(sentences_path, "w") as f:
json.dump(sentence_entries, f, indent=2, ensure_ascii=False)
print(f"Saved {sentences_path}")
# Create a minimal dummy VQA dataset so the GoldData DataLoader doesn't crash
dummy_vqa = [
{
"id": "dummy_001",
"image": get_image_relpath(all_captions[0]["image_id"]),
"conversations": [
{"from": "human", "value": "<image>\nWhat is in this image?"},
{"from": "gpt", "value": all_captions[0]["llava_caption"][:100]},
],
}
]
vqa_path = os.path.join(args.output_dir, "dummy_vqa.json")
with open(vqa_path, "w") as f:
json.dump(dummy_vqa, f, indent=2)
print(f"Saved {vqa_path}")
if args.symlink_dir:
os.makedirs(args.symlink_dir, exist_ok=True)
for entry in pos_neg_entries + sentence_entries:
src = get_image_path(entry["image"].split("/")[-1].replace(".jpg", ""))
if src and os.path.exists(src):
dst = os.path.join(args.symlink_dir, os.path.basename(src))
if not os.path.exists(dst):
os.symlink(os.path.abspath(src), dst)
print(f"Created symlink directory at {args.symlink_dir}")
if __name__ == "__main__":
main()