File size: 6,189 Bytes
b91f38a | 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 | #!/usr/bin/env python3
"""
prepare_cxr_from_single_json.py
Utility to split a single JSON/JSONL dataset into train/valid and prepare
the minimal files expected by the CXR-only training path in RadIR:
- train.jsonl / valid.jsonl : each line JSON with keys `name` and `img_path`
- csv/all.csv : contains a `File Path` column listing sample names (train set)
- npy/all.npy : similarity matrix (float32, values in [0,1]) for the CSV rows
By default similarity is computed with TF-IDF + cosine similarity. If you
have `sentence-transformers` installed you can pass `--sim-method st` to use
a semantic embedding model (better but requires the package and a model).
Examples:
python scripts/prepare_cxr_from_single_json.py \
--input dataset/all.jsonl \
--out /tmp/radir_debug --split 0.9 --seed 42 --sim-method tfidf
"""
import os
import sys
import json
import argparse
from pathlib import Path
import numpy as np
def read_input(path):
path = Path(path)
text = path.read_text()
# Try JSONL (one json per line)
try:
lines = [l.strip() for l in text.splitlines() if l.strip()]
data = [json.loads(l) for l in lines]
return data
except Exception:
pass
# Try a single JSON array
try:
data = json.loads(text)
if isinstance(data, list):
return data
except Exception:
pass
raise ValueError(f"Unsupported input format for {path}. Expect JSONL or JSON array of objects.")
def ensure_name_field(entry):
if 'name' in entry:
return entry['name']
if 'img_path' in entry:
return os.path.basename(entry['img_path'])
raise ValueError('Entry lacks both `name` and `img_path`')
def save_jsonl(entries, path):
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
with p.open('w') as f:
for e in entries:
f.write(json.dumps(e, ensure_ascii=False) + '\n')
def save_csv_filepaths(names, path):
import csv
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
with p.open('w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['File Path'])
for n in names:
writer.writerow([n])
def compute_similarity_tfidf(texts):
try:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
except Exception as e:
raise RuntimeError('TF-IDF dependencies missing: install scikit-learn') from e
vec = TfidfVectorizer(max_features=8192)
X = vec.fit_transform(texts)
sim = cosine_similarity(X)
# clamp to [0,1]
sim = (sim + 1.0) / 2.0
sim = np.clip(sim, 0.0, 1.0).astype(np.float32)
return sim
def compute_similarity_sentence_transformers(texts, model_name='all-MiniLM-L6-v2'):
try:
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
except Exception as e:
raise RuntimeError('sentence-transformers or sklearn not installed') from e
model = SentenceTransformer(model_name)
emb = model.encode(texts, convert_to_numpy=True, show_progress_bar=False)
sim = cosine_similarity(emb)
sim = (sim + 1.0) / 2.0
sim = np.clip(sim, 0.0, 1.0).astype(np.float32)
return sim
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--input', required=True, help='path to input JSON or JSONL file (list of samples)')
parser.add_argument('--out', required=True, help='output directory to write train/valid/json/csv/npy')
parser.add_argument('--split', type=float, default=0.9, help='train fraction')
parser.add_argument('--seed', type=int, default=42)
parser.add_argument('--sim-method', choices=['tfidf', 'st'], default='tfidf', help='similarity method: tfidf (fast) or st (sentence-transformers)')
parser.add_argument('--st-model', default='all-MiniLM-L6-v2', help='sentence-transformers model name (if --sim-method st)')
args = parser.parse_args()
data = read_input(args.input)
# Normalize entries: ensure fields name, img_path, text (text optional)
entries = []
for d in data:
e = dict(d)
if 'img_path' not in e and 'image' in e:
e['img_path'] = e.pop('image')
if 'name' not in e:
e['name'] = ensure_name_field(e)
entries.append(e)
import random
random.seed(args.seed)
indices = list(range(len(entries)))
random.shuffle(indices)
n_train = int(len(entries) * args.split)
train_idx = set(indices[:n_train])
train_entries = [entries[i] for i in indices[:n_train]]
valid_entries = [entries[i] for i in indices[n_train:]]
out = Path(args.out)
out.mkdir(parents=True, exist_ok=True)
# For RadIR Stage2 we need train.jsonl/valid.jsonl entries with keys name & img_path
train_jsonl = out / 'train.jsonl'
valid_jsonl = out / 'valid.jsonl'
save_jsonl([{'name':e['name'], 'img_path': e['img_path'], 'text': e.get('text','')} for e in train_entries], train_jsonl)
save_jsonl([{'name':e['name'], 'img_path': e['img_path'], 'text': e.get('text','')} for e in valid_entries], valid_jsonl)
# Create csv dir and npy dir
csv_dir = out / 'csv'
npy_dir = out / 'npy'
csv_dir.mkdir(parents=True, exist_ok=True)
npy_dir.mkdir(parents=True, exist_ok=True)
# CSV should list File Path as names (use train set by default)
names = [e['name'] for e in train_entries]
save_csv_filepaths(names, csv_dir / 'all.csv')
# compute similarity matrix from texts (train set rows order)
texts = [e.get('text','') or '' for e in train_entries]
if len(texts) == 0:
sim = np.eye(len(names), dtype=np.float32)
else:
if args.sim_method == 'st':
sim = compute_similarity_sentence_transformers(texts, model_name=args.st_model)
else:
sim = compute_similarity_tfidf(texts)
np.save(npy_dir / 'all.npy', sim.astype(np.float32))
print('Wrote:')
print(' -', train_jsonl)
print(' -', valid_jsonl)
print(' -', csv_dir / 'all.csv')
print(' -', npy_dir / 'all.npy')
if __name__ == '__main__':
main()
|