Huyt's picture
Upload build_split.py with huggingface_hub
3eeaf03 verified
Raw
History Blame Contribute Delete
4.22 kB
"""Build a deduplicated, group-aware, stratified split of the rice leaf disease dataset.
Two dataset defects drive the design:
1. 1,839 exact-duplicate files (6.3%), 45 hash groups carrying conflicting labels.
2. Augmentation series: files named PREFIX<N>_<ID> share base image <ID>. Splitting
these randomly leaks a leaf across train/test and inflates accuracy.
Grouping key is therefore the base image, and dedup happens before splitting.
"""
import hashlib
import json
import re
from collections import defaultdict
from pathlib import Path
from sklearn.model_selection import StratifiedGroupKFold
ROOT = Path(__file__).parent / "data_raw" / "Rice Disease"
OUT = Path(__file__).parent / "splits.json"
DROP_CLASSES = {"Leaf Smut"} # ~97% mislabeled blast images; see model card
SEED = 42
# BLAST1_074.jpg and BLAST5_074.jpg are augmentations of the same base leaf "074".
# The series index is always 1 digit; the {1,2} bound keeps 14-digit camera timestamps
# (IMG20201109210545_00.jpg) from being misread as a series and collapsing 322 distinct
# Neck Blast photos into one group.
SERIES = re.compile(r"^([A-Za-z]+)(\d{1,2})_(\d+)\.[^.]+$")
# brownspot_orig_001.jpg / brownspot_rotated_001.jpg: same leaf, two variants.
VARIANT = re.compile(r"^(.*?)_(?:orig|rotated)_(\d+)\.[^.]+$", re.I)
def group_key(cls: str, name: str) -> str:
m = SERIES.match(name)
if m:
return f"{cls}|{m.group(1).upper()}|{m.group(3)}"
m = VARIANT.match(name)
if m:
return f"{cls}|{m.group(1).upper()}|{m.group(2)}"
return f"{cls}|uniq|{name}"
def main():
files, labels, groups = [], [], []
by_hash = defaultdict(list)
for cdir in sorted(p for p in ROOT.iterdir() if p.is_dir()):
if cdir.name in DROP_CLASSES:
continue
for f in sorted(cdir.iterdir()):
if f.is_file():
by_hash[hashlib.md5(f.read_bytes()).hexdigest()].append((cdir.name, f))
dropped_conflict = 0
for _, entries in by_hash.items():
classes = {c for c, _ in entries}
if len(classes) > 1:
# Same bytes, two labels: at least one is wrong and we cannot tell which.
dropped_conflict += len(entries)
continue
cls, f = entries[0] # keep one representative of each duplicate group
files.append(str(f.relative_to(ROOT.parent)))
labels.append(cls)
groups.append(group_key(cls, f.name))
classes = sorted(set(labels))
cls_to_idx = {c: i for i, c in enumerate(classes)}
y = [cls_to_idx[c] for c in labels]
# 70/15/15 by groups: 20 folds of ~5% each, 3 to val, 3 to test, rest to train.
sgkf = StratifiedGroupKFold(n_splits=20, shuffle=True, random_state=SEED)
folds = [b.tolist() for _, b in sgkf.split(files, y, groups)]
val_idx = set().union(*folds[0:3])
test_idx = set().union(*folds[3:6])
train_idx = set(range(len(files))) - val_idx - test_idx
split = {
"classes": classes,
"seed": SEED,
"dropped_classes": sorted(DROP_CLASSES),
"dropped_conflicting_files": dropped_conflict,
"train": sorted(train_idx),
"val": sorted(val_idx),
"test": sorted(test_idx),
"files": files,
"labels": y,
"groups": groups,
}
OUT.write_text(json.dumps(split))
# Verify the property we built this for.
g = {name: set() for name in ("train", "val", "test")}
for name in g:
for i in split[name]:
g[name].add(groups[i])
leaks = (g["train"] & g["val"]) | (g["train"] & g["test"]) | (g["val"] & g["test"])
print(f"kept {len(files)} images / {len(set(groups))} base groups / {len(classes)} classes")
print(f"dropped: {dropped_conflict} files in cross-label hash conflicts")
print(f"train {len(train_idx)} | val {len(val_idx)} | test {len(test_idx)}")
print(f"group leakage across splits: {len(leaks)} (must be 0)")
print(f"\n{'class':<22}{'train':>7}{'val':>6}{'test':>6}")
for c in classes:
ci = cls_to_idx[c]
row = [sum(1 for i in split[s] if y[i] == ci) for s in ("train", "val", "test")]
print(f"{c:<22}{row[0]:>7}{row[1]:>6}{row[2]:>6}")
if __name__ == "__main__":
main()