Условие
Classifier на 100k изображениях. Val accuracy 95%, test 96%, прод 78%. Подозрение на leakage. Как искать?
Решение
Подход
Возможные leakage:
- Точные дубликаты в train/val/test.
- Augmentations того же фото (разный crop, brightness) попавшие в разные splits.
- Серии фото (burst-mode, видео-кадры): соседние очень похожи.
- Метаданные: timestamp/camera/EXIF одинаковы для train и test изображений из одной фотосессии.
Поиск дубликатов
Точные: MD5 / SHA hash. Просто.
import hashlib
import os
from collections import defaultdict
hashes = defaultdict(list)
for path in all_paths:
h = hashlib.md5(open(path,'rb').read()).hexdigest()
hashes[h].append(path)
duplicates = {h: paths for h, paths in hashes.items() if len(paths) > 1}Near-duplicates (resized, recompressed): perceptual hash (pHash, dHash):
from PIL import Image
import imagehash
phashes = {p: imagehash.phash(Image.open(p)) for p in all_paths}
# Hamming distance между hashes
def near_dups(phashes, threshold=5):
pairs = []
items = list(phashes.items())
for i in range(len(items)):
for j in range(i+1, len(items)):
if items[i][1] - items[j][1] < threshold:
pairs.append((items[i][0], items[j][0]))
return pairsSemantic-near-duplicates (different angle of same subject): embedding similarity.
import torch
from torchvision import models, transforms
model = models.resnet50(weights='IMAGENET1K_V2').eval()
model.fc = torch.nn.Identity()
preprocess = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225])
])
# Получаем embeddings, ищем cosine close pairs через FAISS
import faiss
embs = compute_embeddings(model, all_paths, preprocess)
index = faiss.IndexFlatIP(embs.shape[1])
embs_normed = embs / np.linalg.norm(embs, axis=1, keepdims=True)
index.add(embs_normed)
D, I = index.search(embs_normed, k=5)
# Если cosine > 0.95 и пары в разных splits — leakageStratified split с дедупом
def split_with_dedup(images, labels, dup_groups, test_size=0.2):
# dup_groups: список наборов индексов «один и тот же объект»
group_ids = assign_group_ids(images, dup_groups)
from sklearn.model_selection import GroupShuffleSplit
gss = GroupShuffleSplit(n_splits=1, test_size=test_size, random_state=42)
tr, te = next(gss.split(images, labels, groups=group_ids))
return tr, teМетаданные
from PIL.ExifTags import TAGS
exif = Image.open(path)._getexif()
# Если несколько изображений с одинаковым DateTimeOriginal и тем же camera serial — серияПодводные камни
- Random split на photo dataset с burst-mode → leakage гарантирован. Используйте GroupKFold по photo-session.
- Web-scraped data: одно фото может встречаться на нескольких сайтах в разных resolutions. Perceptual hash найдёт.
- Augmented data leakage: если делаете augmentation до split — синт. версии оригинала разлетаются по splits. Augmentation только на train fold.
- Label leakage: filename содержит class (
dog_0001.jpg) — модель может выучить через filename embedding. Прекратить использовать filenames. - Production drift: 78% на проде vs 96% на test — это не только leakage, может быть domain shift. Проверьте отдельно.
Эталонный ответ
Источники: точные дубли (MD5), near-dups (perceptual hash pHash + Hamming), semantic-near-dups (embedding cosine), серии фото (EXIF), augmented copies (если augment до split). Решение: dedup по pHash и embedding, GroupKFold по photo-session, augmentation только на train fold.