Одинаковые объекты под разными id: проверяем пересечение train и test
После случайного split модель отлично классифицирует документы. Номера строк не пересекаются, но один текст был загружен дважды под разными id. Модель проверяется на копии материала, который уже видела.
Проверять объект, а не только строку
Уникальный row_id гарантирует уникальность записи в таблице, но не уникальность исходного документа. Повторная загрузка, исправленное имя файла и копия из другого канала создают новые записи того же содержания. Для оценки обобщения на новые документы такие копии должны оставаться по одну сторону разделения.
Совпадение обычных табличных признаков не всегда означает дубль: два разных клиента могут иметь одинаковый возраст и доход. Поэтому сигнатуру выбирают по происхождению объекта. Для документа точное содержимое является сильным кандидатом; похожие тексты требуют отдельной проверки.
Сначала найти пересечение содержимого
В примере три документа, каждый загружен дважды. Начальное разбиение кладёт одну копию в train, другую в test. Пересечение id пусто, пересечение текста содержит все документы. Затем создаём группу по точному тексту и делим группы целиком через GroupShuffleSplit.
Для большого корпуса можно предварительно группировать по хешу, но спорные совпадения проверять по содержимому. Хеш ускоряет поиск кандидатов, а политика дублирования остаётся предметным решением.
import pandas as pd
from sklearn.model_selection import GroupShuffleSplit
x = pd.DataFrame({"row_id": [1, 2, 3, 4, 5, 6],
"text": ["invoice A", "invoice B", "letter C", "invoice A", "invoice B", "letter C"]})
train0, test0 = x.iloc[:3], x.iloc[3:]
assert set(train0.row_id).isdisjoint(test0.row_id)
leaked = set(train0.text) & set(test0.text)
groups, _ = pd.factorize(x["text"])
split = GroupShuffleSplit(n_splits=1, test_size=1/3, random_state=7)
tr, te = next(split.split(x, groups=groups))
print({"initial_shared_documents": sorted(leaked), "train_ids": x.iloc[tr].row_id.tolist(),
"test_ids": x.iloc[te].row_id.tolist()})
assert len(leaked) == 3
assert set(x.iloc[tr].text).isdisjoint(x.iloc[te].text)
assert len(tr) + len(te) == len(x)Новое разделение сохраняет все записи, но один текст больше не появляется с обеих сторон. Число независимых документов равно трём, а не шести. Увеличение количества копий не увеличивает объём независимой проверки.
Обработать близкие копии
Удаление пробелов, колонтитулов и номеров страниц помогает искать почти одинаковые документы, но способно объединить разные объекты. Сначала сохраните сырой текст и покажите примеры групп владельцу данных. Подтверждённые семейства копий становятся ключом разделения.
Если одна группа получила разные метки, выясните причину до обучения. Это может быть ошибка разметки, разная версия документа или задача, где контекст вне текста определяет ответ. Механическое удаление одной метки скрывает проблему.
Групповое разделение не заменяет временное, если модель должна работать на будущих периодах. Возможно, потребуются обе границы: копии не пересекаются, а проверка расположена позже обучения. Сохраните правила вместе с manifest разделения.
После исправления пересчитайте baseline и модель на одинаковом новом тесте. Снижение метрики показывает изменение проверки; сравнивать его с прежним числом как регрессию модели некорректно.
Закрепить на практике
В бесплатном уроке утечки вы разберёте неправильное разделение и попадание ответа в признаки. После этой основы примените проверку содержимого из статьи к объектам, которые могут загружаться повторно. Откройте бесплатный урок и задачи. Полная последовательность тем — в бесплатном курсе.
Продолжить на практике
- Машинное обучение с нуля на Python: первая модель ↗
Бесплатный курс машинного обучения на Python: обучите первую модель в scikit-learn, проверьте её качество и решите 106 задач в браузере.
