Кросс-валидация

Материал из llmdocs — справочник разработчика
Портал: ML и MLOps
Кросс-валидация
Назначениеоценка обобщающей способности модели
Основной вариант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-кратная кросс-валидация:

  1. перемешать данные и разбить на k равных частей (фолдов);
  2. для каждого i от 1 до k обучить модель на всех фолдах, кроме i-го, и измерить качество на i-м;
  3. усреднить 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-кратное обучение слишком дорого.

См. также

Источники

  1. scikit-learn — Cross-validation: evaluating estimator performance
  2. scikit-learn — Tuning the hyper-parameters of an estimator
  3. scikit-learn — Common pitfalls and recommended practices
  4. Hastie, Tibshirani, Friedman — The Elements of Statistical Learning, ch. 7
  5. Cawley, Talbot — On Over-fitting in Model Selection (JMLR, 2010)