Перейти к содержимому
5 мин чтения

Загрязнение тестового набора уже испортило ваш eval?

Загрязнение тестового набора завышает баллы LLM. Проверяйте источники, дубликаты, шаблоны и устойчивость моделей к перефразам.

Загрязнение тестового набора уже испортило ваш eval?

Высокий балл LLM ничего не говорит о качестве модели, пока вы не отделили обобщение от узнавания. Если тестовые формулировки, ответы или их близкие варианты уже жили в открытом интернете, в датасете для дообучения или в промптах вашей команды, модель может пройти проверку по памяти. В продакшене она встретит другой документ, другой порядок условий и другое исключение, а ваша таблица с лидерами этого не заметит.

Загрязнение тестового набора нельзя убрать одним дедупликатором. Точные копии встречаются редко и ловятся легко. Опаснее другой случай: пятьдесят задач построены по одному публичному шаблону, синтетический генератор переставил местами условия, а редактор заменил названия сущностей. Для человека это разные строки. Для модели, которая уже видела исходный бенчмарк и тысячи его пересказов, это один знакомый экзамен.

В отчёте GPT-4 OpenAI описывала отдельную проверку на пересечения с оценочными данными. Это правильная инженерная привычка, но не доказательство чистоты любого внешнего теста: для закрытой модели сторонняя команда не видит её полный корпус предобучения. Авторы работы "Proving Test Set Contamination in Black-Box Language Models" прямо строят методы для чёрного ящика именно потому, что веса и обучающие данные часто недоступны.

Загрязнение и утечка ломают оценку по-разному

Загрязнение означает, что модель могла встретить оценочный материал до вашего запуска. Источник может быть публичным: GitHub-репозиторий, страница соревнования, набор на Hugging Face, обсуждение с правильными ответами, учебный конспект, который переписал вопросы. В таком случае вы не обязаны искать виноватого внутри компании, но обязаны перестать называть балл чистой проверкой обобщения.

Утечка между разработкой и тестом происходит внутри вашего процесса. Команда добавила тестовый пример в few-shot промпт. Аналитик отправил выгрузку подрядчику для разметки. Инженер включил ошибки теста в датасет SFT. Руководитель показал десяток самых неприятных кейсов поставщику модели и затем теми же строками измерил улучшение. Это уже не риск происхождения данных, а прямой дефект контура разработки.

Смешивать эти случаи вредно. При внешнем загрязнении вы меняете доказательную силу бенчмарка и добавляете независимые проверки. При внутренней утечке вы аннулируете результат для затронутых записей, выясняете путь данных и меняете доступы. Фраза "мы же не обучали модель" не оправдывает ситуацию, когда тестовые примеры жили в системном промпте или в наборе для ручного выбора модели.

Есть ещё третий случай, который часто маскируют словом "утечка": повторяющийся шаблон. Модель не видела именно эту строку, но десятки примеров используют одинаковую операцию, одинаковое распределение ответов и одинаковый стиль подсказки. Формально пересечения нет. Практически один навык получает несоразмерный вес, и итоговый балл становится хрупким.

Один высокий балл не отделяет память от навыка

Модель, которая знает ответ, и модель, которая умеет решить задачу, могут выдать одинаковую строку. Обычный accuracy не различает эти причины. Поэтому бесполезно спрашивать модель: "Ты видела этот вопрос?" Она не хранит проверяемый журнал источников и может уверенно ошибиться в ответе о собственной истории обучения.

Нужна проверка на инвариантность. Сохраните логическую задачу, но измените форму, которая не должна влиять на ответ. Переставьте условия. Уберите фирменные имена. Замените бытовой сюжет на нейтральный. Добавьте нерелевантную деталь. Попросите вернуть ответ в другой форме. Если исходник модель решает безупречно, а на эквивалентной версии резко проседает, вы получили сигнал знакомства, а не доказательство. Для решения о закупке этого сигнала уже достаточно.

Работа Sainz и соавторов "NLP Evaluation in trouble" называет самым тяжёлым случаем обучение на тестовом сплите с последующей оценкой на нём же и подчёркивает, что масштаб проблемы сложно измерить. Практический вывод проще академической формулировки: не публикуйте один процент и не делайте вид, что он измеряет одну вещь.

Смотрите на четыре значения для каждого семейства заданий:

  • результат на оригинальных строках;
  • результат на эквивалентных перефразах;
  • результат на новых задачах того же рабочего процесса;
  • долю случаев, где меняется не только ответ, но и уверенность модели.

Разрыв между первым и вторым значением полезнее среднего балла на публичном наборе. Он показывает, насколько ваша оценка зависит от поверхности текста. Разрыв между вторым и третьим значением показывает другое: переносится ли навык на реальные входящие документы, которые не похожи на учебные задания.

Чеклист начинается с происхождения каждой строки

Проверка набора не начинается с эмбеддингов. Сначала выясните, откуда взялась каждая запись. Если команда не может ответить на этот вопрос, она не знает, что оценивает.

Для каждой строки заведите реестр с полями item_id, source, source_url_or_ticket, created_at, author_or_owner, license_or_access, transformation, cluster_id, answer_owner и release_status. В поле transformation пишите не "очищено", а конкретное действие: "перевод с английского", "перефразировано редактором", "сгенерировано по шаблону v3", "вырезано из обращения клиента, PII удалены".

Потом пройдите этот чеклист. Он короткий, но команда должна отвечать на каждый пункт документом, а не воспоминанием.

  1. Проверьте, лежал ли исходный вопрос или его перевод в открытом репозитории, статье, учебном материале, соревновании или публичном наборе.
  2. Проверьте, попадал ли пример в промпты, демонстрационные запросы, ручные тесты, SFT, preference-данные и отчёты о прошлых экспериментах.
  3. Найдите точные и почти точные дубликаты внутри test, а также пересечения test с train, dev и набором ошибок.
  4. Объедините перефразы и синтетические варианты в кластеры, затем посчитайте, какой вес каждый кластер имеет в финальной метрике.
  5. Отметьте строки, где ответ угадывается по позиции варианта, длине, шаблону JSON или повторяющемуся слову в вопросе.

Последний пункт недооценивают. В тестах с вариантами ответа правильный вариант нередко чаще стоит в одной позиции, содержит более подробную формулировку или повторяет термин из вопроса. Это не загрязнение предобучения, но такой дефект даёт ложный балл ровно так же: модель учится обходному признаку вместо нужной операции.

Не пытайтесь закрыть проблему только лицензией или доступом. Набор мог быть закрыт для вас и уже находиться в обучающих данных поставщика. И наоборот, публичный набор может оставаться пригодным для регрессии, если вы честно называете его публичным, не используете как единственный критерий и сопровождаете закрытой проверкой.

Точные совпадения ловятся скриптом, а не внимательным чтением

Чтение таблицы глазами хорошо ловит очевидные копии и плохо ловит пунктуацию, Unicode, перевод и перестановку фраз. Ниже минимальная проверка, которую стоит запускать при каждом обновлении eval-набора. Она ищет точные совпадения после нормализации и пары с высокой символьной похожестью.

import csv
import re
import unicodedata
from difflib import SequenceMatcher
from collections import defaultdict

def normalize(text: str) -> str:
    text = unicodedata.normalize("NFKC", text).lower()
    text = re.sub(r"\s+", " ", text).strip()
    text = re.sub(r"[^\w\s]", "", text)
    return text

def read_items(path: str):
    with open(path, newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f))

train = read_items("train.csv")
test = read_items("test.csv")

train_index = defaultdict(list)
for row in train:
    train_index[normalize(row["prompt"])].append(row["id"])

for test_row in test:
    prompt = normalize(test_row["prompt"])
    if prompt in train_index:
        print({
            "type": "exact_overlap",
            "test_id": test_row["id"],
            "train_ids": train_index[prompt]
        })

    for train_row in train:
        score = SequenceMatcher(
            None, prompt, normalize(train_row["prompt"])
        ).ratio()
        if score >= 0.92:
            print({
                "type": "near_duplicate",
                "test_id": test_row["id"],
                "train_id": train_row["id"],
                "similarity": round(score, 3)
            })

Вывод должен иметь форму записи, а не общего числа:

{'type': 'exact_overlap', 'test_id': 't-041', 'train_ids': ['tr-882']}
{'type': 'near_duplicate', 'test_id': 't-117', 'train_id': 'tr-301', 'similarity': 0.947}

Не ставьте порог 0.92 в регламент и не думайте, что он универсален. Для коротких вопросов это слишком грубо, для длинных юридических фрагментов он может пропустить копию с одной заменённой фразой. Возьмите первые 50 срабатываний, разметьте их человеком как "дубликат", "один шаблон", "случайное сходство" и подберите порог для своего типа текста.

Этот скрипт не ищет утечки в предобучении модели. Он делает более приземлённую работу: не даёт вашей команде случайно оценивать на собственном train и не позволяет десяткам копий одного кейса выдать себя за широкий набор задач. Это уже устраняет большую часть нелепых отчётов.

Семантический поиск помогает, но не выносит вердикт

Сводите eval к одному API
Единый OpenAI-совместимый эндпоинт объединяет OpenAI, Anthropic, Google, DeepSeek, xAI и других провайдеров.

Эмбеддинги находят пары, которые не совпадают по словам: перевод, переставленные условия, переписанный сюжет. Они полезны для очереди ручной ревизии. Они не доказывают, что два текста измеряют один навык, и не доказывают, что модель видела один из них до запуска.

Постройте для каждого примера вектор по prompt и отдельно по связке prompt + expected_answer. Второй индекс важен: две формулировки могут быть похожи, но требовать разных решений. Затем выгрузите ближайших соседей из test против train, dev, SFT-данных и публичных источников, если у вас есть их локальная копия. Человек должен решить, что делать с каждой парой.

Плохая практика выглядит так: команда берёт cosine similarity выше 0.85, удаляет всё подряд и объявляет набор чистым. Так вы выбросите разные, но тематически похожие записи, оставите опасные структурные перефразы и потеряете следы в журнале. Хорошая практика хранит решение ревьюера: keep, merge_cluster, remove_from_test, rewrite, investigate_source.

Авторы статьи "How Contaminated Is Your Benchmark?" предлагают Kernel Divergence Score как способ измерять возможное загрязнение через различие в поведении представлений до и после дообучения. Это интересный исследовательский метод, но он требует доступа к модели и этапу fine-tuning. Для выбора API-модели он не заменяет контрольный набор и преобразованные задачи.

Не выдавайте семантический поиск за судебную экспертизу. Его роль проста: находить места, где человек потратит внимание с пользой.

Синтетические варианты часто сохраняют чужой экзамен

Команды любят синтетику, потому что она быстро наращивает объём и не требует показывать реальное обращение клиента. Это разумно, пока вы не путаете объём с независимостью.

Представьте публичную задачу: "Клиент просит отменить перевод после 18:00, что должен сделать оператор?" Генератор создаёт сто вариантов: меняет банк на маркетплейс, перевод на возврат, 18:00 на 17:30, имя клиента и валюту. Все варианты всё ещё проверяют знакомое правило в той же форме. Если модель видела исходную задачу, документацию и множество подобных бенчмарков, синтетический набор лишь умножил один сигнал.

Проверяйте у синтетических данных не уникальность строк, а независимость решения. Для каждого шаблона ответьте письменно на три вопроса:

  • какой факт или ограничение должен извлечь исполнитель;
  • что меняется между вариантами и может изменить правильный ответ;
  • какой неправильный путь должен стать правдоподобным после изменения.

Если меняется только имя сущности или число, не добавляйте вариант в тест. Оставьте его в наборе для нагрузочного теста, проверки формата или регрессии. Он может быть полезен, но не имеет права увеличивать уверенность в качестве модели.

Особенно опасны шаблоны с фиксированным ответом. Например, в 80 процентах случаев система должна ответить "передать оператору", потому что генератор строит задания вокруг запрещённого действия. Модель может научиться безопасному слову и набрать высокий score, не разобрав ни одного условия. Считайте распределение классов по каждому шаблону, а не только по датасету целиком.

Повторяющиеся формулировки дают одному навыку лишний голос

Разделяйте нагрузку ключами
Лимиты на уровне ключа позволяют ограничивать нагрузку отдельных eval-прогонов.

Если двадцать записей отличаются только номером договора, они не равны двадцати независимым наблюдениям. Это один сценарий, повторённый двадцать раз. Средний accuracy при таком устройстве завышает уверенность и скрывает провалы на редких сценариях.

Кластер должен строиться по операции, а не по теме. Запросы "извлечь ИИН из скана", "сверить сумму в счёте" и "найти дату в договоре" относятся к документообороту, но проверяют разные действия. Зато "определи, разрешён ли перевод по лимиту" с разными именами и суммами чаще всего один кластер, если правила и ловушки одинаковы.

В отчёте выводите два результата. Первый, построчный, нужен для поиска конкретных ошибок. Второй, кластерный, сначала вычисляет метрику внутри каждого кластера, затем усредняет кластеры с равным весом. Если разница заметна, не спорьте о том, какой балл "настоящий". Зафиксируйте причину: один или несколько сценариев доминируют в наборе.

Для критичных процессов полезнее ещё один срез: worst-cluster score. Он отвечает на неприятный, но деловой вопрос: какой тип запроса модель обрабатывает хуже всего? В банке, healthcare или госуслугах средний балл не компенсирует один повторяемый провал на чувствительной операции.

Динамические наборы вроде Dynabench строились вокруг примеров, на которых конкретная модель ошибается, при сохранении человеческой решаемости. Идея не в том, чтобы бесконечно ловить модель на трюках. Она в том, чтобы тест не застывал в форме, которую разработчики и модели успели выучить.

Закрытый набор нужен для решения, а публичный для наблюдения

Добавьте open-weight модели
На собственной инфраструктуре AI Router хостятся Llama 4, Qwen 3, Gemma 4, DeepSeek V3.2 и Phi-5.

Публичные бенчмарки полезны. На них удобно отслеживать регрессию, сравнивать конфигурации промптов и видеть грубые отличия между моделями. Но ими нельзя единолично утверждать, что модель справится с вашими обращениями, договорами, внутренними правилами или локальным языковым контекстом.

Закрытый набор не обязан быть огромным. Он обязан быть хорошо отобранным. В нём должны быть реальные операции, известные варианты ошибок, свежие документы, конфликтующие условия и случаи, где правильный ответ зависит от локальной политики. Не добавляйте туда всё подряд. Добавляйте то, за что владелец процесса готов отвечать.

Разделите доступы. Разработчик, который меняет промпт, может видеть класс ошибки и агрегированную обратную связь, но не всегда обязан видеть исходный текст и эталон. Иначе через несколько итераций закрытый тест превращается в тренировочный материал без формального обучения.

Для LLM-приложений полезно держать три слоя:

  • открытый набор для регрессии и воспроизводимых сравнений;
  • рабочий набор с обезличенными, но знакомыми команде кейсами для отладки;
  • закрытый набор для решения о выпуске и периодической независимой проверки.

AI Router может упростить сам прогон одного и того же eval через разные совместимые с OpenAI модели, когда команде нужно сравнить кандидатов без переписывания клиентского кода. Но шлюз не сделает набор независимым за вас: происхождение примеров, доступы и кластерный подсчёт остаются работой команды.

Отчёт должен показывать сомнения, а не прятать их в примечание

Плохой отчёт сообщает: "Модель A получила 84,2%, модель B получила 81,7%". Из него нельзя понять, на чём построена разница, какие данные могли быть публичными и сколько однотипных записей попало в среднее.

Хороший отчёт содержит четыре отдельных блока. В первом укажите версии модели, системный промпт, параметры генерации, дату запуска и способ оценки. Во втором покажите происхождение набора: долю публичных, внутренних, синтетических и закрытых записей. В третьем приведите post-level и cluster-level метрики вместе с разрывом оригинал против перефраза. В четвёртом перечислите исключённые строки и причину исключения.

Не прячьте внизу фразу "возможна контаминация". Если она способна перевернуть выбор модели, она должна стоять рядом с итоговой таблицей. Руководитель, который видит это ограничение до закупки, примет более полезное решение, чем руководитель, которому покажут красивый балл и объяснят проблему после инцидента.

Начните с одного действия: возьмите последние 100 тестовых записей, постройте кластеры, сделайте по одному эквивалентному перефразу для каждого важного кластера и пересчитайте результат. Если лидер меняется, вы не нашли досадную погрешность. Вы обнаружили, что старый тест измерял знакомство с формой задания сильнее, чем работу модели.

Часто задаваемые вопросы

Можно ли использовать публичный бенчмарк для выбора LLM?

Да. Публичность сама по себе не делает набор бесполезным, но меняет смысл результата: вы проверяете смесь способности модели и её возможного знакомства с материалом. Используйте такой набор для регрессии и сравнения конфигураций, а решение о запуске в продакшен подтверждайте закрытым набором из своих задач.

Как доказать, что закрытая модель видела тестовые данные?

Для открытых весов можно искать совпадения с известными корпусами предобучения и проверять историю датасета. Для API-модели вы обычно не можете доказать состав обучающих данных, поэтому ищите признаки: дословное продолжение, резкое преимущество на старых публичных заданиях и исчезновение преимущества после перефразирования.

Что делать, если в тестовом наборе много похожих вопросов?

Сначала удалите точные дубликаты и записи с одинаковым ответом после нормализации. Затем объединяйте семантически одинаковые варианты в кластеры и оставляйте в финальном подсчёте один представитель кластера, иначе один знакомый шаблон даст вам десятки голосов.

Снижает ли синтетический набор риск загрязнения?

Синтетика полезна, когда она создаёт новые факты, новые ограничения или новые комбинации документов. Она бесполезна как защита, если генератор лишь меняет имена, порядок слов и числа в публичной задаче: модель узнает скелет задания, а не обязана помнить каждую строку.

Что такое парафразный тест в оценке LLM?

Парафразный тест сохраняет правильный ответ, но меняет подачу задачи: контекст, порядок условий, отвлекающие детали и форму вопроса. Если модель уверенно отвечает только на исходник, её высокий балл нельзя считать чистой оценкой прикладного навыка.

Нужен ли отдельный закрытый тест после публичного бенчмарка?

Да, если этот набор отделён от разработки и не попадает в промпты, few-shot примеры, файлы разметки и отчёты команды. Лучше всего хранить его у владельца продукта или независимой группы качества и запускать только на кандидатах, которые уже прошли отбор.

Какие метрики показывают загрязнение тестового набора?

Сравнивайте не только средний балл, но и разрыв между оригиналом и преобразованными вариантами, стабильность по кластерам и число провалов на критичных сценариях. Модель, которая выиграла один процентный пункт на старом публичном наборе, но ломается на ваших перефразах, не выиграла ничего полезного.

Чем загрязнение отличается от утечки между train и test?

Если открытые данные попали в предобучение, это не утечка из вашей команды, но это всё равно загрязнение бенчмарка. Если тестовые записи оказались в SFT, preference-данных, промптах или в цикле ручной доводки вашей команды, это уже утечка оценки в разработку и её нужно разбирать как дефект процесса.

Какие поля нужны в реестре тестовых примеров?

Достаточно хранить исходный текст, нормализованную форму, источник, дату появления в наборе, идентификатор кластера и причину включения. Для закрытого набора добавьте владельца, список допущенных людей и журнал запусков, иначе через полгода никто не поймёт, когда тест перестал быть закрытым.

Как часто нужно проверять eval-набор на загрязнение?

Проверка полезна перед первым большим сравнением моделей, после добавления нового источника данных и перед публикацией результата за пределами команды. Не ждите подозрительно высокого балла: к этому моменту выбор модели, бюджет и дорожная карта уже могут опираться на неверный вывод.