Кросс-валидация
| Назначение | оценка обобщающей способности модели |
|---|---|
| Основной вариант | k-fold (обычно k = 5 или 10) |
| Варианты | stratified, group, time series, leave-one-out, nested |
| Реализация | scikit-learn (sklearn.model_selection) |
| Связанное | подбор гиперпараметров, переобучение, утечка данных |
Кросс-валидация (перекрёстная проверка, англ. cross-validation) — способ оценить, насколько хорошо модель машинного обучения будет работать на новых данных. Данные несколько раз делят на обучающую и проверочную части, модель обучают и проверяют на каждом разбиении, а результаты усредняют. В отличие от единственного разбиения на train/test, кросс-валидация использует все данные и для обучения, и для проверки и показывает не только среднее качество, но и его разброс.
Зачем
Качество модели на обучающих данных ничего не говорит о качестве на новых: сложная модель может просто запомнить примеры (см. Переобучение и регуляризация). Поэтому часть данных откладывают. Но одно случайное разбиение ненадёжно: на маленьком наборе оценка сильно зависит от того, какие примеры попали в тест. Кросс-валидация уменьшает эту случайность и позволяет честно сравнивать модели и гиперпараметры.[4]
k-fold
Самый распространённый вариант — k-кратная кросс-валидация:
- перемешать данные и разбить на k равных частей (фолдов);
- для каждого i от 1 до k обучить модель на всех фолдах, кроме i-го, и измерить качество на i-м;
- усреднить k оценок и посчитать стандартное отклонение.
Обычно берут k = 5 или 10: модель обучается на 80–90% данных, а вычислений в 5–10 раз больше, чем при одном разбиении. Крайний случай — leave-one-out (k равно числу примеров): почти несмещённая, но дорогая и шумная оценка.
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
scores = cross_val_score(RandomForestClassifier(), X, y, cv=5, scoring="f1")
print(f"F1 = {scores.mean():.3f} ± {scores.std():.3f}")
Метрику выбирают по задаче (см. Метрики качества классификации).[1]
Варианты разбиения
| Вариант | Когда использовать |
|---|---|
| StratifiedKFold | классификация с несбалансированными классами — доля классов в каждом фолде одинакова |
| GroupKFold | у примеров есть группы (пациент, пользователь, документ), которые нельзя разрывать между train и test |
| TimeSeriesSplit | временные ряды — обучение только на прошлом, проверка на будущем |
| RepeatedKFold | маленький набор — несколько повторов с разными перемешиваниями |
| ShuffleSplit | большой набор — несколько случайных разбиений произвольного размера |
Для временных данных обычное перемешивание недопустимо: модель «подглядит» в будущее, и оценка окажется оптимистичной. TimeSeriesSplit расширяет обучающее окно шаг за шагом, как в реальной эксплуатации.
Подбор гиперпараметров
Кросс-валидация — основа подбора гиперпараметров: для каждой комбинации считается средняя оценка по фолдам, и выбирается лучшая.[2]
from sklearn.model_selection import GridSearchCV
search = GridSearchCV(
estimator=model,
param_grid={"max_depth": [3, 5, 8], "n_estimators": [200, 500]},
cv=5, scoring="roc_auc", n_jobs=-1,
)
search.fit(X_train, y_train)
print(search.best_params_, search.best_score_)
Есть тонкость: если много раз выбирать лучшую конфигурацию по одной и той же кросс-валидации, её оценка становится смещённой вверх — модель «переобучается на валидацию».[5] Честная оценка требует отложенного тестового набора, который не участвует в подборе, или вложенной кросс-валидации (nested CV): внутренний цикл подбирает параметры, внешний оценивает весь процесс.
Утечка данных
Самая частая ошибка — выполнить предобработку (масштабирование, отбор признаков, заполнение пропусков, балансировку классов) на всём наборе до разбиения. Тогда статистики тестовых фолдов влияют на обучение, и оценка завышена.[3] Решение — объединить предобработку и модель в Pipeline, чтобы на каждом фолде всё обучалось только на обучающей части:
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
pipe = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
cross_val_score(pipe, X, y, cv=StratifiedKFold(5, shuffle=True, random_state=0))
Другие источники утечки — дубликаты между фолдами, признаки, вычисленные с использованием целевой переменной, и данные одного объекта по разные стороны разбиения (решается GroupKFold).
Кросс-валидация в бустинге и глубоком обучении
Для градиентного бустинга кросс-валидация, помимо оценки, помогает выбрать число деревьев через раннюю остановку на каждом фолде; предсказания моделей разных фолдов часто усредняют (out-of-fold ансамбль), что популярно на соревнованиях Kaggle. В глубоком обучении, где одна модель обучается часами, обычно ограничиваются одним валидационным набором — данных достаточно, а k-кратное обучение слишком дорого.
См. также
Источники
- scikit-learn — Cross-validation: evaluating estimator performance
- scikit-learn — Tuning the hyper-parameters of an estimator
- scikit-learn — Common pitfalls and recommended practices
- Hastie, Tibshirani, Friedman — The Elements of Statistical Learning, ch. 7
- Cawley, Talbot — On Over-fitting in Model Selection (JMLR, 2010)