Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Кросс-валидация и подбор гиперпараметров

Кросс-валидация и подбор гиперпараметров: как оценивать модель честно

Кросс-валидация: KFold, Stratified, TimeSeriesSplit, групповая валидация, утечка данных при подборе и корректный порядок пайплайна.

Прямой ответ

Кросс-валидация — оценка модели на частях данных, которые не участвовали в её обучении. Она нужна, потому что одна случайная валидационная выборка даёт шумную оценку: разница в 2–3 % между двумя разбиениями обычна, и по одному прогону нельзя понять, стало лучше или хуже.

Схемы разбиения:

  • KFold. Данные делятся на k частей; модель обучается на k−1 и проверяется на оставшейся, по очереди. Стандарт — 5 частей.
  • StratifiedKFold. Доли классов в каждой части сохраняются. Обязателен для классификации с дисбалансом.
  • TimeSeriesSplit. Обучение только на прошлом, проверка на будущем. Для временных рядов — единственный корректный вариант.
  • GroupKFold. Группы (клиенты, устройства, дни) не пересекаются между фолдами. Нужен, когда несколько наблюдений относятся к одной сущности.

Главная ошибка — утечка при подборе параметров. Если масштабирование, импутация или отбор признаков выполнены по всем данным до разбиения, информация из валидации попадает в обучение, и оценка становится оптимистичной. Правильно — выполнять все преобразования внутри каждого фолда, что обеспечивает Pipeline.

from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV, StratifiedKFold

pipe = Pipeline([
    ("impute", SimpleImputer(strategy="median")),
    ("scale",  StandardScaler()),
    ("clf",    LogisticRegression(max_iter=1000)),
])

grid = {"clf__C": [0.1, 1, 10], "clf__penalty": ["l1", "l2"], "clf__solver": ["liblinear"]}
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
search = GridSearchCV(pipe, grid, cv=cv, scoring="roc_auc", n_jobs=-1)
search.fit(X_train, y_train)
print(search.best_score_, search.best_params_)

Порядок действий, который предотвращает большинство ошибок:

  1. Разделить данные на обучение и отложенный тест один раз, до любых экспериментов.
  2. Все эксперименты и подбор параметров вести на обучающей части через кросс-валидацию.
  3. Отложенный тест использовать один раз — для финальной оценки выбранной модели.
  4. Зафиксировать результат и не возвращаться к тесту для «ещё одной попытки».

Практика: выбор схемы под задачу

Дисбаланс классов. Используйте StratifiedKFold, иначе в фолд может не попасть положительный класс. Дополнительно следите за метриками: при сильном дисбалансе точность бесполезна — см. метрики при дисбалансе.

Повторяющиеся сущности. Если у одного клиента 50 транзакций, случайное разбиение положит часть его транзакций в обучение, часть в валидацию. Модель запомнит клиента, и оценка будет завышенной. Решение — GroupKFold по идентификатору клиента.

Временные данные. TimeSeriesSplit с нарастающим окном обучения. Дополнительно полезно оставлять «зазор» между обучением и проверкой, если признаки содержат лаги.

from sklearn.model_selection import GroupKFold

gcv = GroupKFold(n_splits=5)
for tr, va in gcv.split(X, y, groups=customer_ids):
    ...

Сколько фолдов брать. 5 — разумный дефолт. 10 даёт меньший разброс оценки, но вдвое дороже. Больше 10 оправдано только при малом объёме данных.

Рекомендации:

  1. Смотрите на разброс, а не только на среднее. Среднее 0.87 ± 0.03 и 0.87 ± 0.005 — разные ситуации; во втором случае разница моделей в 0.5 % уже значима.
  2. Не подбирайте по тесту. Если после просмотра результата на тесте вы вернулись к параметрам, тест перестал быть независимым.
  3. Фиксируйте random_state. Иначе сравнение двух конфигураций измеряет шум разбиения.
  4. Логируйте прогоны. История подбора с параметрами и метриками нужна для воспроизведения — см. MLflow Tracking.
  5. Для бустинга используйте раннюю остановку внутри валидации. Как это сочетается с подбором — в статье про градиентный бустинг.

Практический сценарий: качество упало после «улучшения» пайплайна

Контекст: добавили отбор признаков по важности и получили рост ROC-AUC с 0.84 до 0.91 на валидации; после выкатки качество осталось 0.83.

  1. Диагностика. Отбор признаков выполнялся по всему датасету до кросс-валидации. Информация о целевой переменной из валидационных фолдов попала в отбор — классическая утечка.
  2. Проверка. Перенесли отбор внутрь Pipeline как шаг SelectKBest. Оценка на кросс-валидации снизилась до 0.845 — то есть «рост» был иллюзией.
  3. Дополнительно. Обнаружили, что данные содержат по несколько записей на клиента; перешли на GroupKFold, оценка стала 0.83 — теперь она совпадает с онлайн-результатом.
  4. Итог. Реального улучшения не было, зато оценка стала достоверной: офлайн и онлайн совпадают.
  5. Профилактика. Ввели правило: любые преобразования данных — только шаги пайплайна, и добавили тест, проверяющий отсутствие пересечения групп между фолдами.

Вывод: рост метрики после изменения пайплайна — не всегда улучшение. Сначала проверьте, не появилась ли утечка.

Как сравнивать протоколы валидации между собой. Если вы меняете схему разбиения (например, переходите со случайной на групповую), сравнивать результаты «до» и «после» некорректно: это разные оценки одной модели. Правильная последовательность — выбрать протокол один раз, до экспериментов, и дальше менять только модель. Смена протокола по ходу работы почти всегда выглядит как падение качества, потому что прежняя оценка была завышенной.

FAQ

Можно ли обойтись одним разбиением на train/test? Для быстрой проверки идеи — да. Для принятия решения о выкатке — нет: оценка на одном разбиении слишком шумная, а при подборе параметров ещё и смещённая.

Что делать, если данных мало? Используйте больше фолдов или Leave-One-Out, но помните: оценка становится дороже и шумнее. Часто полезнее упростить модель и усилить регуляризацию, чем искать идеальный протокол валидации.

Как сравнивать две модели корректно? На одинаковых фолдах с одинаковым random_state и с учётом разброса. Если разница меньше разброса, модели эквивалентны по качеству, и выбирать стоит по простоте и стоимости эксплуатации.

Нужен ли отдельный тест при кросс-валидации? Да. Кросс-валидация участвует в выборе модели, поэтому её оценка слегка оптимистична. Независимый отложенный тест, использованный один раз, даёт честную оценку того, чего ждать в проде.

Источники: scikit-learn docs (cross validation, grid search, common pitfalls) — ссылки в шапке.