Divisões que dizem a verdade
Encontra imagens quase duplicadas que criam fugas entre treino e validação e divide os dados pela unidade que será nova na implantação.
Se um modelo obtém 95% na validação e 60% em produção, normalmente é porque a divisão dos dados enganou. Imagens quase idênticas — fotogramas adjacentes de vídeo, a mesma câmara no mesmo dia — ficaram nos dois lados, pelo que a validação testou a memorização em vez da capacidade de generalização. Vais encontrar exatamente essa fuga num conjunto de dados real e depois corrigi-la com uma divisão por grupos.
Prático
Três divisões, três funções#
| Divisão | Quem aprende com ela | Quando a consultas |
|---|---|---|
| Treino | O modelo: os pesos são ajustados aqui | A cada época |
| Validação | Você: hiperparâmetros, épocas, tamanho do modelo, limiares | Após cada experimento |
| Teste | Ninguém: ele informa o número final | Uma vez, depois que todas as decisões forem tomadas |
Cada decisão que você toma ao observar uma divisão vaza um pouco dela para o modelo. Esse é o papel do conjunto de validação. Ajuste o modelo com o conjunto de teste e o número deixa de ser uma estimativa honesta — você terá sobreajustado às próprias escolhas. No Ultralytics, model.val(split="test") avalia a divisão de teste definida no YAML do seu conjunto de dados.
Encontre o vazamento em um conjunto de dados real#
As imagens de pílulas médicas têm os nomes Frame_0.jpg, Frame_4.jpg, Frame_8.jpg — são quadros de um vídeo. Isso é um sinal de alerta: quadros adjacentes são quase idênticos, e uma divisão aleatória os coloca nos dois conjuntos. Verifique comparando pequenas impressões digitais em escala de cinza de cada imagem de validação com cada imagem de treinamento:
from pathlib import Path
import numpy as np
from PIL import Image
from ultralytics.data.utils import check_det_dataset
data = check_det_dataset("medical-pills.yaml")
def thumbnail(path):
"""A 32x18 grayscale fingerprint: enough to spot near-identical frames."""
return np.asarray(Image.open(path).convert("L").resize((32, 18)), dtype=float)
train = {p.name: thumbnail(p) for p in sorted(Path(data["train"]).glob("*.jpg"))}
pairs = []
for p in sorted(Path(data["val"]).glob("*.jpg")):
v = thumbnail(p)
name, diff = min(((n, np.abs(v - t).mean()) for n, t in train.items()), key=lambda x: x[1])
pairs.append((diff, p.name, name))
pairs.sort()
print(f"{sum(d < 5 for d, _, _ in pairs)} of {len(pairs)} val images have a near-twin in train (difference < 5)")
for diff, val_name, train_name in pairs[:3]:
print(f" {val_name} ~ {train_name} difference {diff:.1f}")17 of 23 val images have a near-twin in train (difference < 5)
Frame_344.jpg ~ Frame_56.jpg difference 0.3
Frame_28.jpg ~ Frame_312.jpg difference 0.5
Frame_24.jpg ~ Frame_308.jpg difference 1.8Uma diferença de 0.3 numa escala de 0–255 significa que as duas imagens são praticamente iguais. Abra val/Frame_24.jpg e train/Frame_308.jpg lado a lado e você verá as mesmas cartelas de comprimidos do mesmo ângulo. 17 das 23 imagens de validação têm uma gêmea no conjunto de treinamento, então a pontuação de validação mede, em grande parte, o quanto o modelo se lembra de imagens que já viu. Isso é aceitável para um conjunto de dados de demonstração rápida — e é exatamente o que você não deve colocar em produção.

A mesma armadilha existe em qualquer ferramenta. A descrição da própria caixa de diálogo diz que ela reatribui aleatoriamente cada imagem, por isso é ideal quando as imagens são fotos independentes — e causa vazamento quando são quadros de vídeo. Primeiro, execute uma verificação de quase duplicatas e mantenha a proporção do conjunto de teste acima de 0% se quiser um número final que ninguém ajustou.
Divida por grupo#
A regra: as divisões devem ser independentes na unidade que será nova na implantação.
| Dados | Dividir por |
|---|---|
| Vídeo | Clipe ou sessão de gravação |
| Conjunto de câmeras fixas | Câmera ou local |
| Cenas que mudam com o tempo | Dia ou semana |
| Dados médicos, esportivos ou de pessoas | Paciente, atleta ou participante |
| Sequências de fotos | Evento de captura |
Os nomes dos arquivos geralmente contêm a chave do grupo. Este exemplo cria 900 imagens de 6 câmeras e compara as duas estratégias:
import random
from collections import defaultdict
# File names carry the unit that must never straddle splits: camera_day_frame.jpg
images = [f"cam{c:02d}_day{d}_{f:04d}.jpg" for c in range(1, 7) for d in range(1, 4) for f in range(50)]
# WRONG: shuffle individual frames, so near-identical neighbors land in both splits
random.seed(0)
shuffled = random.sample(images, len(images))
train, val = shuffled[: int(0.8 * len(images))], shuffled[int(0.8 * len(images)) :]
shared = {i.split("_")[0] for i in train} & {i.split("_")[0] for i in val}
print(f"per-image split: {len(shared)} of 6 cameras appear in both train and val")
# RIGHT: shuffle whole cameras, then expand them back into images
groups = defaultdict(list)
for image in images:
groups[image.split("_")[0]].append(image)
cameras = random.sample(sorted(groups), len(groups))
train = [i for cam in cameras[:5] for i in groups[cam]]
val = [i for cam in cameras[5:] for i in groups[cam]]
print(f"per-camera split: train {len(train)} images, val {len(val)} images, val cameras {cameras[5:]}")per-image split: 6 of 6 cameras appear in both train and val
per-camera split: train 750 images, val 150 images, val cameras ['cam05']Com a divisão por imagem, a validação nunca vê uma câmera nova. Com a divisão por câmera, a pontuação estima o que acontece com uma câmera que o modelo nunca viu — o número de que você precisa.
Armadilhas#
Sintoma: validação muito melhor que o primeiro teste em campo. Solução: encontre a chave de agrupamento — muitas vezes oculta nos nomes dos arquivos ou nos metadados de captura —, faça a divisão com base nela e execute a verificação de quase duplicatas para confirmar.
Sintoma: uma classe rara sem AP ou AP calculada com base em duas instâncias. Solução: depois da divisão por grupos, conte cada classe em cada partição e troque grupos inteiros até que todas as classes apareçam em proporções semelhantes. sklearn.model_selection.StratifiedGroupKFold automatiza esse processo.
Sintoma: o valor do teste aumenta gradualmente a cada experimento. Solução: congele o conjunto de teste, selecione-o com condições semelhantes às de produção e informe os resultados uma vez por experimento concluído. Em conjuntos de dados pequenos, calcule a média de várias divisões com validação cruzada k-fold em vez de reutilizar uma só.
Divida o conjunto de dados corretamente aborda proporções e como aplicar tudo isso a um conjunto de dados completo.
Mostrar solução
Uma verificação de vazamento que funciona com qualquer lista de nomes de arquivos, desde que receba uma função que extraia a chave do grupo:
def leaked_groups(splits, key):
"""Return every group key that appears in more than one split."""
owner, leaks = {}, set()
for split, files in splits.items():
for name in files:
group = key(name)
if owner.setdefault(group, split) != split:
leaks.add(group)
return sorted(leaks)
# Substitua estas listas pelos nomes dos seus arquivos, por exemplo, [p.name for p in Path("images/train").glob("*.jpg")]
splits = {
"train": ["cam01_day1_0001.jpg", "cam01_day1_0002.jpg", "cam02_day1_0001.jpg"],
"val": ["cam03_day2_0001.jpg", "cam01_day1_0003.jpg"],
"test": ["cam04_day3_0001.jpg"],
}
print(leaked_groups(splits, key=lambda name: name.split("_")[0])) # agrupar por câmera['cam01']cam01 aparece tanto em train quanto em val, então esta divisão tem vazamento. Mova todas as imagens cam01 para uma única divisão e execute novamente até que seja impresso []. Para medical-pills, em que os quadros de um vídeo mostram novamente a mesma perspectiva, os nomes dos arquivos não contêm uma chave de grupo clara: grave novos vídeos de validação de embalagens diferentes ou agrupe as imagens pela impressão digital de quase duplicatas e atribua cada grupo inteiro a uma única divisão.