Прямой ответ
Кросс-валидация — оценка модели на частях данных, которые не участвовали в её обучении. Она нужна, потому что одна случайная валидационная выборка даёт шумную оценку: разница в 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_)
Порядок действий, который предотвращает большинство ошибок:
- Разделить данные на обучение и отложенный тест один раз, до любых экспериментов.
- Все эксперименты и подбор параметров вести на обучающей части через кросс-валидацию.
- Отложенный тест использовать один раз — для финальной оценки выбранной модели.
- Зафиксировать результат и не возвращаться к тесту для «ещё одной попытки».
Практика: выбор схемы под задачу
Дисбаланс классов. Используйте 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 оправдано только при малом объёме данных.
Рекомендации:
- Смотрите на разброс, а не только на среднее. Среднее 0.87 ± 0.03 и 0.87 ± 0.005 — разные ситуации; во втором случае разница моделей в 0.5 % уже значима.
- Не подбирайте по тесту. Если после просмотра результата на тесте вы вернулись к параметрам, тест перестал быть независимым.
- Фиксируйте
random_state. Иначе сравнение двух конфигураций измеряет шум разбиения. - Логируйте прогоны. История подбора с параметрами и метриками нужна для воспроизведения — см. MLflow Tracking.
- Для бустинга используйте раннюю остановку внутри валидации. Как это сочетается с подбором — в статье про градиентный бустинг.
Практический сценарий: качество упало после «улучшения» пайплайна
Контекст: добавили отбор признаков по важности и получили рост ROC-AUC с 0.84 до 0.91 на валидации; после выкатки качество осталось 0.83.
- Диагностика. Отбор признаков выполнялся по всему датасету до кросс-валидации. Информация о целевой переменной из валидационных фолдов попала в отбор — классическая утечка.
- Проверка. Перенесли отбор внутрь
Pipelineкак шагSelectKBest. Оценка на кросс-валидации снизилась до 0.845 — то есть «рост» был иллюзией. - Дополнительно. Обнаружили, что данные содержат по несколько записей на клиента; перешли на
GroupKFold, оценка стала 0.83 — теперь она совпадает с онлайн-результатом. - Итог. Реального улучшения не было, зато оценка стала достоверной: офлайн и онлайн совпадают.
- Профилактика. Ввели правило: любые преобразования данных — только шаги пайплайна, и добавили тест, проверяющий отсутствие пересечения групп между фолдами.
Вывод: рост метрики после изменения пайплайна — не всегда улучшение. Сначала проверьте, не появилась ли утечка.
Как сравнивать протоколы валидации между собой. Если вы меняете схему разбиения (например, переходите со случайной на групповую), сравнивать результаты «до» и «после» некорректно: это разные оценки одной модели. Правильная последовательность — выбрать протокол один раз, до экспериментов, и дальше менять только модель. Смена протокола по ходу работы почти всегда выглядит как падение качества, потому что прежняя оценка была завышенной.
FAQ
Можно ли обойтись одним разбиением на train/test? Для быстрой проверки идеи — да. Для принятия решения о выкатке — нет: оценка на одном разбиении слишком шумная, а при подборе параметров ещё и смещённая.
Что делать, если данных мало? Используйте больше фолдов или Leave-One-Out, но помните: оценка становится дороже и шумнее. Часто полезнее упростить модель и усилить регуляризацию, чем искать идеальный протокол валидации.
Как сравнивать две модели корректно?
На одинаковых фолдах с одинаковым random_state и с учётом разброса. Если разница меньше разброса, модели эквивалентны по качеству, и выбирать стоит по простоте и стоимости эксплуатации.
Нужен ли отдельный тест при кросс-валидации? Да. Кросс-валидация участвует в выборе модели, поэтому её оценка слегка оптимистична. Независимый отложенный тест, использованный один раз, даёт честную оценку того, чего ждать в проде.
Источники: scikit-learn docs (cross validation, grid search, common pitfalls) — ссылки в шапке.