Собесов

Сценарий ML: leakage из-за схожих изображений в train/test

ML / Data ScienceComputer VisionСложнаяSenior

Условие

Classifier на 100k изображениях. Val accuracy 95%, test 96%, прод 78%. Подозрение на leakage. Как искать?

Решение

Подход

Возможные leakage:

  1. Точные дубликаты в train/val/test.
  2. Augmentations того же фото (разный crop, brightness) попавшие в разные splits.
  3. Серии фото (burst-mode, видео-кадры): соседние очень похожи.
  4. Метаданные: 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 pairs

Semantic-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 — leakage

Stratified 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 — серия

Подводные камни

  1. Random split на photo dataset с burst-mode → leakage гарантирован. Используйте GroupKFold по photo-session.
  2. Web-scraped data: одно фото может встречаться на нескольких сайтах в разных resolutions. Perceptual hash найдёт.
  3. Augmented data leakage: если делаете augmentation до split — синт. версии оригинала разлетаются по splits. Augmentation только на train fold.
  4. Label leakage: filename содержит class (dog_0001.jpg) — модель может выучить через filename embedding. Прекратить использовать filenames.
  5. 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.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти