Кросс-валидация в Python: K-Fold, стратифицированная, LOOCV
Кросс-валидация в Python: K-Fold, Stratified K-Fold, Leave-One-Out, вложенная CV и пайплайны с scikit-learn — с запускаемыми примерами.
Кросс-валидация — это стандартный способ оценить, насколько хорошо модель машинного обучения будет работать на новых данных. Вместо того чтобы полагаться на одно разбиение на обучающую и тестовую выборки — которое может дать слишком оптимистичную или пессимистичную оценку в зависимости от того, какие образцы попали в какую часть — кросс-валидация обучает и оценивает модель несколько раз на разных разделах данных и усредняет результаты.
На этой странице рассматриваются:
- Почему одного разбиения на обучающую и тестовую выборки недостаточно
- K-Fold кросс-валидация и как выбрать
k - Стратифицированный K-Fold для несбалансированных распределений классов
- Leave-One-Out (LOO) кросс-валидация для небольших наборов данных
- Оценка нескольких метрик с помощью
cross_validate - Использование
Pipelineвнутри кросс-валидации для предотвращения утечки данных - Вложенная кросс-валидация для несмещённой настройки гиперпараметров
Все примеры используют scikit-learn и встроенный набор данных Iris, поэтому вы можете запустить их сразу без каких-либо загрузок.
Зачем нужна кросс-валидация
Простой подход к оценке разбивает данные один раз, обучает модель на одной части и проверяет на другой. Полученная оценка сильно зависит от того, какие образцы попали в каждую часть — удачное разбиение может сделать слабую модель хорошей; неудачное — плохой.
Кросс-валидация решает эту проблему, повторяя процесс обучения/тестирования k раз, каждый раз используя другую часть данных в качестве тестовой выборки. Итоговая оценка — это среднее по всем фолдам, которое значительно стабильнее одного измерения.
Кросс-валидация также максимально эффективно использует ограниченные данные: каждый образец используется как для обучения, так и для оценки в ходе всего эксперимента.
См. страницу разбиения на обучающую и тестовую выборки — более простую базовую технику, которую кросс-валидация улучшает.
K-Fold кросс-валидация
K-Fold — наиболее широко используемая стратегия кросс-валидации. Данные делятся на k равных фолдов. На каждой из k итераций:
- Один фолд выделяется в качестве тестовой выборки.
- Оставшиеся
k - 1фолдов образуют обучающую выборку. - Модель обучается с нуля и оценивается на тестовом фолде.
После k итераций вы получаете k оценок. Их среднее — это оценка качества на кросс-валидации; их стандартное отклонение показывает, насколько стабильна эта оценка на разных срезах данных.
При k = 5 и наборе данных из 150 образцов каждый фолд содержит 30 образцов (20 %) для тестирования и 120 образцов (80 %) для обучения.
Базовый пример K-Fold
from sklearn.model_selection import KFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
# Load the built-in Iris dataset (150 samples, 4 features, 3 classes)
iris = load_iris()
X, y = iris.data, iris.target
model = LogisticRegression(max_iter=200)
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=kfold, scoring='accuracy')
print("Fold scores:", scores.round(4))
# Fold scores: [1. 1. 0.9333 0.9667 0.9667]
print("Mean accuracy: %.4f Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9733 Std: 0.0249Функция cross_val_score обрабатывает все итерации внутренне. Ключевые параметры:
| Параметр | Назначение |
|---|---|
estimator | Любая модель scikit-learn (или Pipeline) |
X, y | Матрица признаков и вектор целевых значений |
cv | Объект кросс-валидации или целое число (например, cv=5) |
scoring | Строка метрики — 'accuracy', 'f1_macro', 'roc_auc' и др. |
Выбор k
- k = 5 или k = 10 рекомендуется для большинства наборов данных. Эти значения обеспечивают хороший компромисс между смещением и дисперсией оценки.
- Большее k (например, 10) даёт меньшее смещение, но большую дисперсию оценки, и требует больше вычислений.
- Меньшее k (например, 3) работает быстрее, но оценка более чувствительна к тому, как были разделены данные.
- Для очень небольших наборов данных (менее ~100 образцов) рассмотрите вместо этого Leave-One-Out.
Ручной просмотр фолдов
Вы можете самостоятельно итерировать по фолдам, когда нужно проверить, что входит в каждое разбиение, или когда вы хотите выполнить нестандартную логику для каждого фолда:
from sklearn.model_selection import KFold
from sklearn.datasets import load_iris
import numpy as np
iris = load_iris()
X, y = iris.data, iris.target
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
for fold, (train_idx, test_idx) in enumerate(kfold.split(X), start=1):
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
print(f"Fold {fold}: train={len(train_idx)} samples, test={len(test_idx)} samples")Вывод:
Fold 1: train=120 samples, test=30 samples
Fold 2: train=120 samples, test=30 samples
Fold 3: train=120 samples, test=30 samples
Fold 4: train=120 samples, test=30 samples
Fold 5: train=120 samples, test=30 samplesСтратифицированная K-Fold кросс-валидация
Обычный K-Fold делит данные по порядку индексов. При несбалансированном распределении классов это может привести к тому, что некоторые фолды будут содержать очень мало примеров миноритарного класса, делая оценку ненадёжной.
Стратифицированный K-Fold гарантирует, что каждый фолд содержит примерно ту же долю каждого класса, что и весь набор данных. Используйте StratifiedKFold всякий раз, когда ваша целевая переменная является категориальной:
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
model = LogisticRegression(max_iter=200)
skfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=skfold, scoring='accuracy')
print("Fold scores:", scores.round(4))
# Fold scores: [1. 0.9667 0.9333 1. 0.9333]
print("Mean accuracy: %.4f Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9667 Std: 0.0298StratifiedKFold — это кросс-валидатор по умолчанию, используемый внутри GridSearchCV и RandomizedSearchCV для задач классификации — в этих контекстах стратификация применяется автоматически.
Leave-One-Out кросс-валидация
Leave-One-Out (LOO) кросс-валидация — это крайний случай: k равно числу образцов. На каждой итерации один образец является тестовой выборкой, а все остальные образцы образуют обучающую выборку. Для набора данных из 150 образцов это означает 150 циклов обучения/оценки.
from sklearn.model_selection import LeaveOneOut, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
model = LogisticRegression(max_iter=200)
loocv = LeaveOneOut()
scores = cross_val_score(model, X, y, cv=loocv, scoring='accuracy')
print(f"Number of folds: {len(scores)}") # 150
print(f"Mean accuracy: {scores.mean():.4f}") # 0.9667
print(f"Std deviation: {scores.std():.4f}") # 0.1795Когда использовать LOO:
- Ваш набор данных содержит менее ~100 образцов и вы не можете позволить себе резервировать данные для тестирования.
- Вы хотите получить оценку качества модели с наименьшим смещением.
Недостатки LOO:
- Очень высокая вычислительная стоимость — модель переобучается
nраз. - Высокая дисперсия оценки: оценка каждого фолда равна 0 или 1 (бинарная классификация) или является единственной точкой, поэтому стандартное отклонение не имеет смысла для отдельных фолдов.
Для большинства наборов данных K-Fold с k=5 или k=10 является лучшим компромиссом.
Оценка нескольких метрик одновременно
cross_val_score может вычислять только одну метрику за вызов. Используйте cross_validate для одновременного вычисления нескольких метрик, а также для получения оценок на обучающей выборке с целью проверки на переобучение:
from sklearn.model_selection import KFold, cross_validate
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
import numpy as np
iris = load_iris()
X, y = iris.data, iris.target
model = LogisticRegression(max_iter=200)
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
cv_results = cross_validate(
model, X, y,
cv=kfold,
scoring=['accuracy', 'f1_macro'],
return_train_score=True,
)
print("Test accuracy: ", cv_results['test_accuracy'].round(4))
# Test accuracy: [1. 1. 0.9333 0.9667 0.9667]
print("Train accuracy:", cv_results['train_accuracy'].round(4))
# Train accuracy: [0.975 0.9583 0.9833 0.975 0.9833]
print("Test F1-macro: ", cv_results['test_f1_macro'].round(4))
# Test F1-macro: [1. 1. 0.9259 0.9691 0.971 ]Сравнение оценок на обучающей и тестовой выборках по фолдам — быстрый способ обнаружить переобучение: если точность на обучающей выборке стабильно намного выше, чем на тестовой, модель запоминает обучающие данные. См. обсуждение смещения и дисперсии для получения дополнительной информации.
Использование Pipeline внутри кросс-валидации
Распространённая ошибка — применять шаги предобработки (например, масштабирование признаков или заполнение пропусков) ко всему набору данных до кросс-валидации. Это приводит к утечке информации из тестового фолда в процесс обучения и даёт завышенную оценку.
Правильный подход — объединить предобработку и модель в Pipeline и передать пайплайн в cross_val_score. scikit-learn независимо переобучает весь пайплайн — включая масштабировщик — внутри каждого фолда:
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
# Correct: preprocessing is fitted only on training folds
pipe = Pipeline([
('scaler', StandardScaler()),
('clf', LogisticRegression(max_iter=200)),
])
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipe, X, y, cv=skf, scoring='accuracy')
print("Fold scores:", scores.round(4))
# Fold scores: [1. 0.9667 0.9 1. 0.9 ]
print("Mean accuracy: %.4f Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9533 Std: 0.0452Всегда используйте Pipeline, когда ваш рабочий процесс включает любой шаг, который обучается на данных (масштабирование, PCA, кодирование, заполнение пропусков).
Вложенная кросс-валидация
Когда вы используете кросс-валидацию одновременно для настройки гиперпараметров и оценки качества модели на тех же данных, вы рискуете переобучиться на валидационных фолдах — выбранные гиперпараметры оказались лучшими именно на этих конкретных разделах, поэтому сообщаемая оценка является завышенной.
Вложенная кросс-валидация разделяет эти две задачи:
- Внутренний цикл: выбор гиперпараметров через поиск по сетке на обучающих фолдах.
- Внешний цикл: оценка лучшей модели, найденной внутренним циклом, на отложенном тестовом фолде.
from sklearn.model_selection import GridSearchCV, StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
# Inner CV: hyperparameter selection
inner_cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=2)
param_grid = {'C': [0.01, 0.1, 1, 10]}
gs = GridSearchCV(
LogisticRegression(max_iter=300),
param_grid,
cv=inner_cv,
scoring='accuracy',
)
# Outer CV: unbiased performance estimation
outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)
nested_scores = cross_val_score(gs, X, y, cv=outer_cv, scoring='accuracy')
print("Nested CV fold scores:", nested_scores.round(4))
# Nested CV fold scores: [0.9667 1. 0.9333 1. 0.9 ]
print("Mean accuracy: %.4f" % nested_scores.mean())
# Mean accuracy: 0.9600Вложенный подход даёт несмещённую оценку качества финальной развёрнутой модели. Используйте его, когда вы публикуете результаты в исследовательском контексте или сравниваете алгоритмы. Для обычного рабочего процесса развёртывания, когда вы всё равно будете переобучать модель на всех доступных данных, обычно достаточно одного внешнего цикла с GridSearchCV. См. страницу поиска по сетке с подробным руководством по настройке гиперпараметров.
Типичные ошибки
Предобработка вне фолда
Применение масштабировщика ко всему набору данных перед вызовом cross_val_score — вместо использования Pipeline — приводит к утечке статистики тестового фолда в обучение. Решение — всегда использовать Pipeline.
Неправильное использование random_state
Если вы установили shuffle=True без random_state, каждый запуск производит другое разбиение и ваши результаты не воспроизводимы. Всегда устанавливайте random_state в фиксированное целое число при публикации результатов.
Интерпретация стандартного отклонения
Высокое стандартное отклонение по фолдам — не всегда плохо: оно может отражать реальную изменчивость в наборе данных (например, некоторые фолды проще других). Смотрите на отдельные оценки фолдов, прежде чем делать выводы.
Кросс-валидация на временных рядах
K-Fold перемешивает данные случайным образом, что для задач с временными рядами приведёт к смешению будущей информации с прошлыми обучающими окнами. Вместо этого используйте TimeSeriesSplit из scikit-learn, который соблюдает временной порядок.
Краткий справочник
| Техника | Когда использовать | Класс scikit-learn |
|---|---|---|
| K-Fold | Выбор по умолчанию для большинства задач регрессии/классификации | KFold |
| Стратифицированный K-Fold | Классификация с несбалансированными классами | StratifiedKFold |
| Leave-One-Out | Очень маленькие наборы данных (< ~100 образцов) | LeaveOneOut |
| Вложенная CV | Несмещённые оценки с настройкой гиперпараметров | GridSearchCV внутри cross_val_score |
| CV для временных рядов | Данные с временным порядком | TimeSeriesSplit |
Связанные темы
- Разбиение на обучающую и тестовую выборки — более простая базовая техника, которую кросс-валидация улучшает
- Поиск по сетке — настройка гиперпараметров, часто используемая вместе с кросс-валидацией
- Логистическая регрессия — один из классификаторов, используемых в примерах выше
- Линейная регрессия — аналог для регрессии, также оцениваемый с помощью кросс-валидации
- Матрица ошибок — разбивка качества по классам для дополнения оценки точности
- Кривая AUC-ROC — ещё одна метрика оценки, которую можно передать в
scoringвcross_val_score