File size: 9,356 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 | #!/usr/bin/env python3
"""
Generic EFUF data formatter for any relation.
Reads all_captions.json + the clean_captions ckpt.json and produces:
pos_neg_synthetic_{train,val}.json – per-subsentence pos/neg entries
sentences_synthetic_{train,val}.json – whole-caption positive entries
dummy_vqa.json – minimal VQA stub
Image split (train vs val) is determined by which subdirectory the JPEG was
saved to by build_hf_dataset.py (images/train/ or images/val/).
Usage:
# Single relation
cd /data/caotue/multilayer-sae
python EFUF/scripts/format_efuf_data.py --relation kitchen_oven
# All 4 relations
python EFUF/scripts/format_efuf_data.py
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../.."))
from experiment.config.relation_config import RelationConfig, get_relation_config, list_relation_keys
EFUF_DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "data")
SUBSENTENCE_SPLITTER = ",.;!?:"
NEGATIVE_SCORE = 0.0
POSITIVE_SCORE = 40.0
SENTENCE_MEAN_POS = 40.0
SENTENCE_MIN_POS = 35.0
def find_subsentence(text: str, target_words: list[str]) -> tuple[int, int] | None:
"""Return (start, end) char indices of the subsentence containing a target word."""
for sub in re.split(f"[{SUBSENTENCE_SPLITTER}]+", text):
for word in target_words:
if re.search(r"\b" + re.escape(word) + r"\b", sub, re.IGNORECASE):
start = text.find(sub)
return start, start + len(sub)
return None
def mentions_object(caption: str, target_words: list[str]) -> bool:
for word in target_words:
if re.search(r"\b" + re.escape(word) + r"\b", caption, re.IGNORECASE):
return True
return False
def get_image_relpath(image_id: str, img_train_dir: str, img_val_dir: str) -> str | None:
if os.path.exists(os.path.join(img_train_dir, f"{image_id}.jpg")):
return f"train/{image_id}.jpg"
if os.path.exists(os.path.join(img_val_dir, f"{image_id}.jpg")):
return f"val/{image_id}.jpg"
return None
def format_relation(relation: str) -> None:
rc = get_relation_config(relation)
data_dir = os.path.join(EFUF_DATA, relation)
captions_path = os.path.join(data_dir, "all_captions.json")
ckpt_path = captions_path + ".ckpt.json"
img_train_dir = os.path.join(data_dir, "images", "train")
img_val_dir = os.path.join(data_dir, "images", "val")
if not os.path.exists(captions_path):
print(f"[{relation}] ERROR: {captions_path} not found — run build_hf_dataset.py first")
return
if not os.path.exists(ckpt_path):
print(f"[{relation}] ERROR: {ckpt_path} not found — run clean_captions.py first")
return
with open(captions_path) as f:
all_captions: list[dict] = json.load(f)
with open(ckpt_path) as f:
ckpt: dict = json.load(f)
target_words = rc.mention_keywords # e.g. ["toilet"]
scene_key = rc.scene_key # e.g. "bathroom"
object_key = rc.object_key # e.g. "toilet"
step2_judge: dict[str, str] = ckpt.get("step2_judge_done", {})
pos_neg_entries: list[dict] = []
sentence_entries: list[dict] = []
stats = {"no_caption": 0, "no_image": 0, "no_mention": 0, "not_hallucinating": 0, "no_subsentence": 0}
for item in all_captions:
image_id = item["image_id"]
caption: str = item.get("llava_caption", "")
scene: int = item.get(scene_key, 0)
obj: int = item.get(object_key, 0)
if not caption:
stats["no_caption"] += 1
continue
rel_path = get_image_relpath(image_id, img_train_dir, img_val_dir)
if rel_path is None:
stats["no_image"] += 1
continue
has_mention = mentions_object(caption, target_words)
if obj == 0 and scene == 1:
# Hallucination candidate: scene present, object absent but caption mentions it
if not has_mention:
# Caption correctly says no object — add as positive sentence entry
sentence_entries.append({
"image": rel_path,
"sentence": caption,
"mean": SENTENCE_MEAN_POS,
"min": SENTENCE_MIN_POS,
})
continue
# Confirmed hallucination requires LLM judge YES
if step2_judge.get(image_id) != "YES":
stats["not_hallucinating"] += 1
continue
result = find_subsentence(caption, target_words)
if result is None:
stats["no_subsentence"] += 1
continue
start, end = result
pos_neg_entries.append({
"image": rel_path,
"sentence": caption[:end].rstrip(),
"position": start,
"score": NEGATIVE_SCORE,
"_type": "negative",
})
elif obj == 1:
# True positive: object present
if not has_mention:
# Caption doesn't mention the object — just use as sentence
sentence_entries.append({
"image": rel_path,
"sentence": caption,
"mean": SENTENCE_MEAN_POS,
"min": SENTENCE_MIN_POS,
})
continue
result = find_subsentence(caption, target_words)
if result is None:
stats["no_subsentence"] += 1
continue
start, end = result
pos_neg_entries.append({
"image": rel_path,
"sentence": caption[:end].rstrip(),
"position": start,
"score": POSITIVE_SCORE,
"_type": "positive",
})
sentence_entries.append({
"image": rel_path,
"sentence": caption,
"mean": SENTENCE_MEAN_POS,
"min": SENTENCE_MIN_POS,
})
else:
# scene=0: non-scene images — use as positive sentence entries if they have good captions
if scene == 0 and obj == 0:
sentence_entries.append({
"image": rel_path,
"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"[{relation}] pos_neg: {len(pos_neg_entries)} ({pos_count} pos, {neg_count} neg)")
print(f"[{relation}] sentences: {len(sentence_entries)}")
print(f"[{relation}] skipped: {stats}")
# Strip internal _type key
final_pos_neg = [{k: v for k, v in e.items() if k != "_type"} for e in pos_neg_entries]
def split_by_folder(entries: list[dict]) -> tuple[list[dict], list[dict]]:
train = [e for e in entries if e["image"].startswith("train/")]
val = [e for e in entries if e["image"].startswith("val/")]
return train, val
train_pn, val_pn = split_by_folder(final_pos_neg)
train_sent, val_sent = split_by_folder(sentence_entries)
print(f"[{relation}] train pos_neg: {len(train_pn)}, val pos_neg: {len(val_pn)}")
print(f"[{relation}] train sent: {len(train_sent)}, val sent: {len(val_sent)}")
for name, data in [
("pos_neg_synthetic_train", train_pn),
("pos_neg_synthetic_val", val_pn),
("sentences_synthetic_train", train_sent),
("sentences_synthetic_val", val_sent),
("pos_neg_synthetic", final_pos_neg),
("sentences_synthetic", sentence_entries),
]:
path = os.path.join(data_dir, f"{name}.json")
with open(path, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# Minimal dummy VQA — GoldData loads this pre-processed format directly,
# so image must be an absolute path (no image_dir_path prepending happens).
first_train = next((e for e in pos_neg_entries + sentence_entries if e["image"].startswith("train/")), None)
first_rel = first_train["image"] if first_train else (final_pos_neg + sentence_entries)[0]["image"]
first_abs = os.path.join(img_train_dir if first_rel.startswith("train/") else img_val_dir,
os.path.basename(first_rel))
dummy_vqa = [{
"input": "Describe this image.",
"output": all_captions[0].get("llava_caption", "An image.")[:100],
"image": first_abs,
}]
vqa_path = os.path.join(data_dir, "dummy_vqa.json")
with open(vqa_path, "w") as f:
json.dump(dummy_vqa, f, indent=2)
print(f"[{relation}] Saved all files to {data_dir}")
def main() -> None:
ap = argparse.ArgumentParser(description="Format EFUF training data for any relation")
ap.add_argument("--relation", default=None, help="Single relation key; omit for all 4")
args = ap.parse_args()
relations = [args.relation] if args.relation else list_relation_keys()
for rel in relations:
format_relation(rel)
print("\nDone.")
if __name__ == "__main__":
main()
|