File size: 6,527 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 | # experiment/probing/build_dctrl.py
"""Build a 1k-image D_ctrl jsonl from CC3M, excluding bathroom/toilet keywords.
Outputs (one line per image):
{"image_id": "<id>", "image_path": "", "caption": "..."}
Note: CC3M is stored as WebDataset tar shards under
<root>/preproc/CC3M-Dataset/cc3m-wds-train/cc3m-train-NNNN.tar
Each tar contains triplets: <key>.jpg, <key>.json, <key>.txt
The .txt file holds the caption; .jpg bytes are embedded in the tar
(no separate on-disk image files). image_path is left as "" because
calibrate_tau.py will open images directly from the tar; downstream
scripts that need actual image bytes should use _open_from_tar().
D_ctrl is consumed by calibrate_tau.py and identify_toilet_features.py
(Method C — differential activation needs a clean control distribution).
"""
from __future__ import annotations
import argparse
import json
import os
import random
import re
import tarfile
EXCLUDE_RE = re.compile(r"\b(bathroom|toilets?|lavatory|restroom)\b", re.IGNORECASE)
# Relative subpath inside the root where the wds shards live.
_WDS_SUBDIR = os.path.join("preproc", "CC3M-Dataset", "cc3m-wds-train")
def main() -> None:
p = argparse.ArgumentParser()
p.add_argument("--cc3m_root", required=True, help="Root of CC3M-Dataset")
p.add_argument("--output", required=True, help="Output jsonl path")
p.add_argument("--n", type=int, default=1000)
p.add_argument("--seed", type=int, default=0)
p.add_argument(
"--cache_dir",
default="outputs/feature_ks/d_ctrl_images/",
help="Directory where extracted JPEG images are cached (one file per image).",
)
args = p.parse_args()
candidates = _scan_cc3m(args.cc3m_root)
print(f"Scanned {len(candidates)} candidates")
rng = random.Random(args.seed)
rng.shuffle(candidates)
kept: list[dict] = []
for c in candidates:
if EXCLUDE_RE.search(c.get("caption", "")):
continue
kept.append(c)
if len(kept) >= args.n:
break
if len(kept) < args.n:
raise RuntimeError(
f"Only {len(kept)}/{args.n} samples pass the exclusion filter"
)
# Extract JPEG bytes for kept records whose image_path is a "tar::key" reference.
os.makedirs(args.cache_dir, exist_ok=True)
_extract_images(kept, args.cache_dir)
out_dir = os.path.dirname(args.output)
if out_dir:
os.makedirs(out_dir, exist_ok=True)
with open(args.output, "w") as f:
for c in kept:
f.write(json.dumps(c) + "\n")
print(f"Wrote {len(kept)} → {args.output}")
def _scan_cc3m(root: str) -> list[dict]:
"""Scan CC3M WebDataset tar shards and return (image_id, image_path, caption) dicts.
Real layout (differs from the JSON-sidecar convention in the plan):
<root>/preproc/CC3M-Dataset/cc3m-wds-train/cc3m-train-NNNN.tar
Each tar entry is one of: <key>.jpg | <key>.json | <key>.txt
The caption is the text in <key>.txt (stripped).
image_path encodes the shard+key as "<tar_path>::<key>" so callers
can re-open the tar to extract the image bytes if needed.
"""
wds_dir = os.path.join(root, _WDS_SUBDIR)
if not os.path.isdir(wds_dir):
raise FileNotFoundError(
f"Expected WebDataset shard directory not found: {wds_dir}"
)
out: list[dict] = []
for fname in sorted(os.listdir(wds_dir)):
if not fname.endswith(".tar"):
continue
tar_path = os.path.join(wds_dir, fname)
try:
with tarfile.open(tar_path, "r") as tf:
# Collect all .txt members; each gives us one (key, caption) pair.
for member in tf.getmembers():
if not member.name.endswith(".txt"):
continue
key = member.name[:-4] # strip ".txt"
fobj = tf.extractfile(member)
if fobj is None:
continue
caption = fobj.read().decode("utf-8", errors="replace").strip()
out.append(
{
"image_id": key,
# Encode location so callers can extract the JPEG.
"image_path": f"{tar_path}::{key}.jpg",
"caption": caption,
}
)
except tarfile.TarError:
continue
return out
def _extract_images(records: list[dict], cache_dir: str) -> None:
"""Extract JPEG bytes from tar shards for each record in *records*.
Records whose ``image_path`` looks like ``"<tar_path>::<entry>"`` are
extracted to ``<cache_dir>/<image_id>.jpg`` and the dict is updated in
place. Records that already point to a real file are left untouched.
Extraction is skipped when the destination file already exists (idempotent).
Tars are opened one at a time in sorted order to avoid repeatedly
reopening the same archive.
"""
# Group by tar path so we open each shard at most once.
from collections import defaultdict
by_tar: dict[str, list[dict]] = defaultdict(list)
for c in records:
ip = c.get("image_path", "")
if "::" in ip:
tar_path, _entry = ip.split("::", 1)
by_tar[tar_path].append(c)
for tar_path in sorted(by_tar):
group = by_tar[tar_path]
# Build a lookup: entry_name -> record
entry_map = {}
for c in group:
_tar_path, entry = c["image_path"].split("::", 1)
dest = os.path.join(cache_dir, c["image_id"] + ".jpg")
if os.path.exists(dest):
c["image_path"] = dest # already cached
else:
entry_map[entry] = (c, dest)
if not entry_map:
continue
try:
with tarfile.open(tar_path, "r") as tf:
for member in tf.getmembers():
if member.name not in entry_map:
continue
c, dest = entry_map[member.name]
fobj = tf.extractfile(member)
if fobj is None:
continue
with open(dest, "wb") as out_f:
out_f.write(fobj.read())
c["image_path"] = dest
except tarfile.TarError as exc:
raise RuntimeError(f"Failed to extract from {tar_path}: {exc}") from exc
if __name__ == "__main__":
main()
|