W3docs

Кросс-валидация в 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 итераций:

  1. Один фолд выделяется в качестве тестовой выборки.
  2. Оставшиеся k - 1 фолдов образуют обучающую выборку.
  3. Модель обучается с нуля и оценивается на тестовом фолде.

После 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.0298

StratifiedKFold — это кросс-валидатор по умолчанию, используемый внутри 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

Связанные темы

Was this page helpful?