Кросс-валидация 🔁
В прошлом уроке ты разбил данные на обучающую и валидационную части и получил на валидации одно число — скажем, accuracy 0,84. Это число выглядело как объективная оценка качества модели. Но стоило поменять random_state при разбиении — и вместо 0,84 неожиданно вышло 0,79. Ты ничего не менял в модели, ты просто иначе перемешал те же самые данные перед тем, как отрезать от них кусок под валидацию. А оценка «поплыла» на пять процентных пунктов.
Дело не в том, что модель нестабильна. Дело в том, что одно фиксированное разбиение — это, по сути, один случайный эксперимент. На маленьком датасете (условно, несколько сотен строк) конкретные точки, попавшие в валидацию, могут оказаться «лёгкими» или «сложными» просто по воле случая, и итоговая метрика будет плясать вместе с ними. Ты не измеряешь качество модели как таковое — ты измеряешь качество модели на этой конкретной, одной-единственной выборке из 20% данных, которая могла оказаться какой угодно.
Логичный вопрос: а что, если не полагаться на одно разбиение, а перебрать несколько разных способов поделить данные на обучение и валидацию, посчитать метрику на каждом и усреднить? Тогда случайность конкретного одного разбиения перестанет решать всё — ты получишь оценку, которая учитывает разброс, а не одну случайную точку внутри этого разброса. Именно эта идея и называется кросс-валидацией (cross-validation, перекрёстной проверкой): вместо одной оценки качества на одном валидационном наборе ты получаешь несколько оценок на нескольких разных наборах и смотришь на их среднее — и, что не менее важно, на то, насколько сильно они между собой расходятся.
В этом уроке ты разберёшь k-fold кросс-валидацию с полной пошаговой трассировкой на игрушечном датасете, её экстремальный частный случай — leave-one-out кросс-валидацию (LOOCV), стратифицированную версию для классификации с несбалансированными классами и отдельную, принципиально другую схему для временных рядов. А в конце увидишь, как ровно эти идеи реализованы в sklearn.model_selection — в KFold, cross_val_score и GridSearchCV — и почему именно кросс-валидация, а не многократное подглядывание в один и тот же валидационный набор, служит правильным инструментом честного подбора гиперпараметров.
История
Идея оценивать модель не на одной, а на многих переиспользуемых выборках гораздо старше современного машинного обучения. В 1949 году статистик Морис Кенуй предложил метод, который позже назовут «складным ножом» (jackknife, название закрепил Джон Тьюки): чтобы оценить смещение и дисперсию статистики, по очереди исключай из выборки по одному наблюдению, пересчитывай статистику на оставшихся и смотри, как сильно она колеблется. Это почти дословно совпадает с идеей leave-one-out, которую ты разберёшь в этом уроке, только придумана она была не для оценки качества предсказательных моделей, а для классической статистики — задолго до того, как кто-либо всерьёз говорил о «машинном обучении».
Сам термин «cross-validation» ввели Фредерик Мостеллер и Джон Тьюки в 1968 году, а строгую постановку задачи выбора модели через перекрёстную проверку в современном виде сформулировали Мервин Стоун и Сеймур Гайссер в статьях 1974–1975 годов: они независимо показали, что если поделить выборку на части, по очереди обучаться на одних и проверяться на других, то получившаяся усреднённая оценка ошибки прогноза оказывается заметно надёжнее, чем ошибка на одном фиксированном отложенном куске данных. Тогда же оформилась и идея k-fold — делить данные не на одну пару обучение/проверка, а на $k$ частей и обходить их по очереди.
Решающий для практики шаг сделал Рон Кохави в 1995 году в статье «A Study of Cross-Validation and Bootstrap for Accuracy Estimation and Model Selection». Он экспериментально сравнил разные значения $k$ и разные схемы оценки качества и показал: для большинства реальных задач 10-fold кросс-валидация даёт хороший баланс между смещением и разбросом оценки при разумных вычислительных затратах. Именно после этой работы $k=10$ (а следом и $k=5$ как более дешёвая альтернатива) стали негласным индустриальным стандартом — тем самым значением по умолчанию, которое ты сегодня видишь в параметре cv=5 почти любой функции sklearn.
k-fold кросс-валидация
Интуиция
Вместо того чтобы один раз случайно отрезать 20% данных под валидацию, а остальные 80% отдать под обучение, k-fold кросс-валидация делит весь датасет на $k$ примерно равных частей — их называют фолдами (folds). Дальше модель обучается и проверяется $k$ раз подряд: на каждом шаге ровно один фолд становится валидационным, а оставшиеся $k-1$ фолдов вместе образуют обучающую выборку. После $k$ таких прогонов у тебя есть $k$ оценок метрики качества — и итоговой оценкой становится их среднее.
Ключевое свойство этой схемы — каждая точка данных побывает в валидации ровно один раз и в обучении — ровно $k-1$ раз. Ни одна точка не остаётся «неиспользованной», и ни одна точка не участвует в проверке дважды. Это принципиально отличается от простого повторения обычного разбиения train_test_split несколько раз со случайными random_state: там одни точки могут случайно попасть в валидацию по нескольку раз, а другие — ни разу.
Алгоритм
k-fold кросс-валидация.
- Раздели весь датасет на $k$ примерно равных частей (фолдов), обычно после случайного перемешивания.
- Для каждого фолда $i$ от 1 до $k$: возьми фолд $i$ как валидационную выборку, а объединение всех остальных $k-1$ фолдов — как обучающую; обучи модель на обучающей выборке и вычисли метрику качества на валидационной.
- После того как все $k$ фолдов по очереди побывали валидационными, усредни полученные $k$ значений метрики — это и есть итоговая CV-оценка качества модели.
- Дополнительно посчитай стандартное отклонение метрики по фолдам — оно показывает, насколько устойчива оценка.
Пример 1: полный трейс k=5 без перемешивания — и почему это плохая идея
Возьмём игрушечный датасет из 15 квартир: площадь (м²) и цена (млн ₽). Строки уже отсортированы по возрастанию площади — это очень частая ситуация с «сырыми» данными, выгруженными из базы без явного перемешивания.
| индекс | площадь, м² | цена, млн ₽ |
|---|---|---|
| 0 | 20 | 2,0 |
| 1 | 24 | 2,3 |
| 2 | 28 | 2,6 |
| 3 | 33 | 3,0 |
| 4 | 37 | 3,3 |
| 5 | 41 | 3,7 |
| 6 | 45 | 4,0 |
| 7 | 50 | 4,4 |
| 8 | 55 | 4,8 |
| 9 | 60 | 5,3 |
| 10 | 65 | 5,7 |
| 11 | 72 | 6,3 |
| 12 | 80 | 7,0 |
| 13 | 90 | 7,8 |
| 14 | 100 | 8,7 |
15 объектов ровно делятся на 5 фолдов по 3 объекта. Если применить KFold(n_splits=5, shuffle=False) — то есть просто нарезать данные последовательными кусками по порядку следования строк, — получится:
-
Фолд 1: валидация — индексы {0, 1, 2} (площадь 20–28 м²), обучение — все остальные 12 объектов.
-
Фолд 2: валидация — индексы {3, 4, 5} (площадь 33–41 м²), обучение — остальные 12.
-
Фолд 3: валидация — индексы {6, 7, 8} (площадь 45–55 м²), обучение — остальные 12.
-
Фолд 4: валидация — индексы {9, 10, 11} (площадь 60–72 м²), обучение — остальные 12.
-
Фолд 5: валидация — индексы {12, 13, 14} (площадь 80–100 м²), обучение — остальные 12.
Обрати внимание на фолд 5: в обучающей выборке этого прогона нет ни одной квартиры площадью больше 72 м². Модель должна предсказать цену квартир в 80, 90 и 100 м², ни разу не увидев на обучении ничего похожего по масштабу — это уже не интерполяция внутри знакомого диапазона, а экстраполяция за его пределы. Ошибка на этом фолде почти наверняка окажется заметно выше, чем на остальных четырёх, — и не потому, что модель плоха, а потому, что нарезка фолдов случайно (точнее, совсем не случайно — из-за отсортированных данных) выстроила себе несправедливо тяжёлый экзамен.
Пример 2: тот же датасет с перемешиванием — как это чинится
Если задать KFold(n_splits=5, shuffle=True, random_state=42), индексы сначала случайно перемешиваются, и только потом режутся на 5 последовательных кусков. Допустим, перемешивание дало такой порядок индексов: [7, 2, 13, 0, 9, 5, 11, 3, 14, 6, 1, 10, 4, 12, 8]. Разбивая этот перемешанный список на куски по 3, получаем фолды:
-
Фолд 1: валидация — индексы {7, 2, 13} (площади 50, 28, 90 м²).
-
Фолд 2: валидация — индексы {0, 9, 5} (площади 20, 60, 41 м²).
-
Фолд 3: валидация — индексы {11, 3, 14} (площади 72, 33, 100 м²).
-
Фолд 4: валидация — индексы {6, 1, 10} (площади 45, 24, 65 м²).
-
Фолд 5: валидация — индексы {4, 12, 8} (площади 37, 80, 55 м²).
Теперь в каждом фолде вперемешку и маленькие, и средние, и большие квартиры — а значит, в обучающей выборке каждого прогона тоже есть представители всего диапазона площадей. Модель больше не вынуждена экстраполировать за пределы того, что видела на обучении. Именно поэтому shuffle=True — разумное значение по умолчанию всякий раз, когда порядок строк в датасете не случаен (а он почти никогда не случаен: данные обычно отсортированы по дате загрузки, по ID, по алфавиту или ещё как-то — и это исключение составляют как раз временные ряды, о которых будет отдельный раздел ниже).
Пример 3: как выглядит итоговая оценка — среднее и разброс по фолдам
Пусть для варианта с перемешиванием (Пример 2) линейная регрессия дала на пяти фолдах такие значения MSE (среднеквадратичной ошибки, в квадратных миллионах рублей): 0,05; 0,03; 0,04; 0,06; 0,07.
Итоговая CV-оценка — это среднее:
$$\text{MSE}_{CV} = \frac{0{,}05 + 0{,}03 + 0{,}04 + 0{,}06 + 0{,}07}{5} = \frac{0{,}25}{5} = 0{,}05$$Стандартное отклонение по пяти значениям (выборочное, со знаменателем $k-1=4$) — примерно $0{,}015$. Итоговый отчёт о качестве модели корректно записывать не одним числом, а парой: $\text{MSE} = 0{,}05 \pm 0{,}015$. Для сравнения — если бы вместо перемешанного варианта ты использовал нарезку без перемешивания из Примера 1, ошибка на «экстраполяционном» пятом фолде вполне могла бы оказаться не 0,07, а, скажем, 0,25 — тогда среднее подскочило бы до $0{,}086$, а разброс — до величины, которая явно сигнализирует: с нарезкой фолдов что-то не так, это не характеристика модели, а артефакт разбиения.
Почему это важно
Одно число без разброса создаёт ложное ощущение точности. Когда ты видишь «accuracy = 0,84», кажется, что это точный, объективный факт о модели. Но за этим числом всегда стоит конкретное разбиение данных, и на другом разбиении число будет другим. Пять или десять оценок с фолдов вместо одной дают тебе не только более устойчивое среднее (усреднение само по себе гасит часть случайного шума одного разбиения), но и честную меру неопределённости — стандартное отклонение по фолдам. Модель со средним качеством 0,84 и разбросом 0,01 — это совсем другая история, чем модель со средним 0,84 и разбросом 0,15: у первой ты можешь доверять итоговой цифре, у второй — нет, даже если средние формально совпадают.
Leave-One-Out кросс-валидация (LOOCV)
Интуиция
Leave-one-out — это k-fold кросс-валидация в предельном, самом мелком варианте: количество фолдов равно количеству объектов в датасете, $k=n$. На каждой итерации ровно один объект становится валидационной выборкой (буквально один!), а все остальные $n-1$ объектов идут в обучение. Затем этот единственный отложенный объект прогоняется через обученную модель, и вычисляется ошибка на нём. После $n$ таких итераций (по одной на каждый объект датасета) все ошибки усредняются.
Интуитивно это самый «щедрый» способ использовать данные для обучения: на каждом шаге модель видит почти весь датасет целиком, за вычетом одной строки. И самый честный способ проверки: каждый объект хотя бы раз оказывается ровно в той ситуации, для которой он и предназначен, — единственным примером, которого модель раньше не видела.
Алгоритм
Leave-one-out кросс-валидация (LOOCV).
- Для каждого объекта $i$ от 1 до $n$ в датасете: возьми объект $i$ как единственный элемент валидационной выборки, а все остальные $n-1$ объектов — как обучающую выборку.
- Обучи модель на этой обучающей выборке и вычисли ошибку предсказания на отложенном объекте $i$.
- Повтори шаги 1–2 для всех $n$ объектов, получив $n$ отдельных значений ошибки.
- Усредни все $n$ значений ошибки — это и есть итоговая LOOCV-оценка качества модели.
Пример 1: полная трассировка LOOCV на датасете из 6 объектов
Возьмём совсем крошечный датасет из 6 объектов с индексами 0–5. LOOCV на нём — это ровно 6 итераций:
-
Итерация 1: валидация — {0}, обучение — {1, 2, 3, 4, 5}.
-
Итерация 2: валидация — {1}, обучение — {0, 2, 3, 4, 5}.
-
Итерация 3: валидация — {2}, обучение — {0, 1, 3, 4, 5}.
-
Итерация 4: валидация — {3}, обучение — {0, 1, 2, 4, 5}.
-
Итерация 5: валидация — {4}, обучение — {0, 1, 2, 3, 5}.
-
Итерация 6: валидация — {5}, обучение — {0, 1, 2, 3, 4}.
Заметь: на каждой итерации обучающая выборка отличается от полного датасета всего одной строкой. Это и есть тот самый смысл «leave-one-out» — «оставить снаружи один объект».
Пример 2: численный расчёт итоговой ошибки
Пусть на этих 6 объектах простая модель регрессии, обученная 6 раз по схеме выше, дала такие квадраты ошибок на отложенном объекте: 0,01; 0,04; 0,02; 0,09; 0,01; 0,03 (условные единицы). Итоговая LOOCV-оценка (MSE) — это среднее по всем 6 значениям:
$$\text{MSE}_{LOOCV} = \frac{0{,}01+0{,}04+0{,}02+0{,}09+0{,}01+0{,}03}{6} = \frac{0{,}20}{6} \approx 0{,}033$$В отличие от 5-fold CV, где у тебя всего 5 значений ошибки для усреднения и оценки разброса, здесь их целых 6 (а на реальном датасете — по числу объектов, то есть сотни или тысячи). Казалось бы, чем больше точек усреднения, тем стабильнее должна быть итоговая оценка. Но здесь скрывается контринтуитивный момент, разобранный в конце этого раздела.
Пример 3: вычислительная цена LOOCV на реалистичном датасете
Пусть у тебя есть датасет из 2 000 объектов, и обучение одной модели (одна попытка fit) занимает 0,5 секунды — вполне реалистичное время для несложной модели на данных среднего размера. Сравним затраты:
-
5-fold CV: 5 обучений × 0,5 секунды = 2,5 секунды.
-
10-fold CV: 10 обучений × 0,5 секунды = 5 секунд.
-
LOOCV: 2000 обучений × 0,5 секунды = 1000 секунд ≈ 16,7 минуты.
Разница между «пятью секундами» и «почти семнадцатью минутами» — это разница между тем, что ты можешь позволить себе прогонять десятки раз в день при экспериментах с признаками и гиперпараметрами, и тем, что придётся запускать один раз и ждать. А если модель — не игрушечная линейная регрессия, а градиентный бустинг или нейросеть с обучением по несколько минут, LOOCV на датасете из тысяч объектов становится попросту нереализуемой по времени.
Почему это важно
LOOCV — почти несмещённая оценка ошибки: поскольку каждая обучающая выборка почти совпадает с полным датасетом ($n-1$ из $n$ объектов), модель обучается практически в тех же условиях, что и финальная модель на всех данных, и оценка её качества систематически не занижается и не завышается из-за нехватки данных на обучении. Но у этой почти идеальной несмещённости есть неприятная оборотная сторона: обучающие выборки на соседних итерациях отличаются друг от друга всего одним объектом, а значит, сами обученные модели и их ошибки на соседних итерациях сильно коррелируют между собой. Из-за этой корреляции усреднение по $n$ сильно похожим друг на друга оценкам не так эффективно «гасит» случайный шум, как усреднение по $k=5$ или $k=10$ заметно более разным, менее коррелированным обучающим выборкам — на практике итоговая LOOCV-оценка нередко оказывается более чувствительной к конкретному датасету (то есть более изменчивой, если бы ты повторил весь эксперимент на другой случайной выборке той же генеральной совокупности), чем оценка обычного k-fold с умеренным $k$. Именно поэтому LOOCV — не универсально «самый точный» метод, а инструмент с собственным компромиссом: минимальное смещение и максимальная вычислительная цена в обмен на не всегда меньшую итоговую дисперсию оценки. На практике LOOCV имеет смысл прежде всего на очень маленьких датасетах (десятки — низкие сотни объектов), где каждая обучающая строка на счету, а вычислительная цена всё ещё терпима.
Стратифицированная k-fold кросс-валидация
Интуиция
Обычный k-fold делит объекты на фолды, не глядя на метки классов. На сбалансированных данных (примерно поровну объектов каждого класса) это не проблема — случайное перемешивание само по себе, скорее всего, распределит классы по фолдам достаточно равномерно. Но если классы сильно разбалансированы — скажем, 3% объектов положительного класса против 97% отрицательного, как часто бывает в детекции мошенничества, дефектов или редких заболеваний, — обычная случайная нарезка фолдов рискует создать фолд, где положительных примеров критически мало или вообще нет.
Стратифицированная k-fold кросс-валидация (Stratified k-fold) устраняет этот риск целенаправленно: при нарезке фолдов она следит, чтобы пропорция классов в каждом отдельном фолде максимально точно повторяла пропорцию классов во всём датасете. Если в целом датасете 20% объектов класса 1, то и в каждом отдельном фолде будет примерно 20% объектов класса 1 — вместо того, чтобы полагаться на удачу случайного перемешивания.
Алгоритм
Стратифицированная k-fold кросс-валидация.
- Раздели объекты датасета на группы по значению целевого класса.
- Внутри каждой группы (класса) отдельно раздели объекты на $k$ примерно равных частей.
- Собери итоговый фолд $i$, объединив $i$-ю часть из каждой группы классов — так пропорция классов внутри фолда $i$ окажется близкой к пропорции классов во всём датасете.
- Дальше обучение и проверка идут так же, как в обычном k-fold: по очереди каждый из $k$ собранных таким образом фолдов становится валидационным, метрика усредняется по всем $k$ прогонам.
Пример 1: почему обычный k-fold может подвести на несбалансированных данных
Возьмём датасет из 15 транзакций, из которых 12 легитимных (класс 0, индексы {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}) и всего 3 мошеннических (класс 1, индексы {12, 13, 14}). При обычном KFold(n_splits=5, shuffle=False) на этих индексах фолды получатся так же, как в самом первом примере урока: {0,1,2}, {3,4,5}, {6,7,8}, {9,10,11}, {12,13,14}. Все три мошеннические транзакции целиком попадают в один-единственный, последний фолд. Это означает, что в четырёх итерациях из пяти модель обучается вообще без единого примера мошенничества — ей физически негде научиться отличать этот класс, — а на пятой итерации, наоборот, модель обучена на всех 12 легитимных, но проверяется сразу на всех 3 мошеннических транзакциях одновременно, без единого мошеннического примера в обучении. Оценка recall (доли пойманных мошенничеств) в первых четырёх итерациях будет попросту не определена (в валидации нет ни одного положительного примера), а результат всей CV окажется бессмысленным.
Пример 2: как те же 3 положительных примера распределятся при стратификации
При StratifiedKFold(n_splits=5) на этом же датасете (12 против 3) алгоритм отдельно делит 3 положительных объекта на 5 частей — при 3 объектах и 5 фолдах поровну не выйдет, но стратификация распределяет их максимально равномерно: три фолда получат по одному положительному объекту, а два фолда — ни одного. Например, такое распределение:
-
Фолд 1: 3 объекта — 2 класса 0 и 1 класса 1 (индекс 12).
-
Фолд 2: 3 объекта — 2 класса 0 и 1 класса 1 (индекс 13).
-
Фолд 3: 3 объекта — 2 класса 0 и 1 класса 1 (индекс 14).
-
Фолд 4: 3 объекта — 3 класса 0, положительных нет.
-
Фолд 5: 3 объекта — 3 класса 0, положительных нет.
При таком крошечном числе положительных примеров абсолютной идеальности всё равно не достичь (3 не делится нацело на 5), но три из пяти фолдов теперь содержат хотя бы по одному положительному объекту в валидации, и — что важнее — обучающая выборка в каждой итерации по-прежнему содержит хотя бы часть положительных объектов вместо их полного отсутствия. На реальных датасетах с сотнями или тысячами объектов такого рода перекос сглаживается ещё сильнее, и стратификация обеспечивает стабильно близкую к идеальной пропорцию классов в каждом фолде.
Пример 3: как стратификация стабилизирует метрику по фолдам
Пусть на датасете из 500 транзакций с долей мошенничества 4% обычный KFold дал по пяти фолдам recall: 0,00; 0,55; 0,70; 0,60; 0,65 (первый фолд — 0,00, потому что в его обучающей выборке случайно оказалось критически мало положительных примеров для нормального обучения модели). Среднее — $(0{,}00+0{,}55+0{,}70+0{,}60+0{,}65)/5 = 0{,}50$, а разброс между фолдами огромный — от 0 до 0,70.
StratifiedKFold на тех же данных мог бы дать recall по фолдам: 0,58; 0,61; 0,57; 0,63; 0,60. Среднее — $0{,}598$, а разброс — буквально несколько сотых. Итоговая оценка не просто выше — она гораздо надёжнее: она отражает реальное поведение модели на данных с сохранённой пропорцией классов, а не артефакт одного неудачно нарезанного фолда.
Почему это важно
Пропуск стратификации на несбалансированных данных — это не мелкая техническая деталь, а способ получить бессмысленную или сильно искажённую оценку качества. Хуже того: если ты используешь такую нестабильную CV-оценку для сравнения двух моделей или подбора гиперпараметров (см. раздел про GridSearchCV ниже), ты рискуешь выбрать худшую модель только потому, что ей повезло с нарезкой фолдов, — а не потому, что она объективно лучше. StratifiedKFold — почти всегда правильный выбор по умолчанию для любой задачи классификации, и тем более обязателен при заметном дисбалансе классов.
Кросс-валидация для временных рядов
Интуиция
Всё, что было описано выше, опирается на одно неявное допущение: объекты датасета независимы друг от друга, и их порядок не имеет значения — поэтому их можно свободно перемешивать и раскидывать по фолдам как угодно. Для временных рядов (продажи по дням, курс валюты по часам, показания датчика по секундам) это допущение ложно в самой своей основе: значение сегодня зависит от значений вчера и позавчера, а обучающие и тестовые примеры образуют не мешок независимых точек, а последовательность с направленной временной связью.
Если применить к временному ряду обычный KFold со случайным перемешиванием, легко получить ситуацию, где модель обучается на данных из будущего (например, за март) и проверяется на данных из прошлого (например, за январь). Формально ошибка на валидации может получиться прекрасной — модель ведь «предсказывает» январь, уже зная мартовские значения через обученные на них закономерности. Но в реальном использовании модель никогда не будет иметь доступа к будущим данным на момент прогноза: это классическая утечка данных из будущего (look-ahead bias), и она делает CV-оценку полностью нечестной — на проде такая модель неизбежно покажет результат намного хуже, чем предсказывала «безупречная» кросс-валидация.
Правильная схема кросс-валидации для временных рядов сохраняет хронологию: обучающая выборка всегда состоит только из данных, которые по времени предшествуют валидационной. Есть два основных варианта такой схемы — расширяющееся окно (expanding window), где обучающая выборка на каждом следующем шаге растёт, вбирая в себя всё больше прошлого, и скользящее окно (rolling / sliding window), где размер обучающей выборки остаётся фиксированным, а окно целиком сдвигается вперёд по времени.
Алгоритм
Кросс-валидация для временных рядов (расширяющееся окно).
- Упорядочи данные строго по времени — никакого перемешивания.
- Зафиксируй начальный размер обучающей выборки и размер тестового блока.
- На первой итерации возьми первый по времени блок данных как обучение, а следующий за ним блок — как валидацию.
- На каждой следующей итерации расширь обучающую выборку, включив в неё предыдущий тестовый блок, и сдвинь тестовый блок дальше во времени.
- Повторяй, пока не закончатся данные; усредни метрику по всем итерациям — только теперь усреднение идёт по «срезам времени», а не по случайным подвыборкам.
Пример 1: расширяющееся окно на 12 точках временного ряда
Возьмём 12 последовательных месячных наблюдений (индексы 0–11 по возрастанию времени). Схема с начальным обучением в 5 точек и тестовым блоком в 1 точку даёт такую трассировку:
-
Итерация 1: обучение — индексы [0, 1, 2, 3, 4], тест — индекс [5].
-
Итерация 2: обучение — индексы [0, 1, 2, 3, 4, 5], тест — индекс [6].
-
Итерация 3: обучение — индексы [0, 1, 2, 3, 4, 5, 6], тест — индекс [7].
-
Итерация 4: обучение — индексы [0, …, 7], тест — индекс [8].
-
Итерация 5: обучение — индексы [0, …, 8], тест — индекс [9].
-
Итерация 6: обучение — индексы [0, …, 9], тест — индекс [10].
-
Итерация 7: обучение — индексы [0, …, 10], тест — индекс [11].
Обучающая выборка нигде не «заглядывает» вперёд теста — она всегда состоит строго из более ранних по времени точек, чем валидируемая. Именно такую схему реализует sklearn.model_selection.TimeSeriesSplit.
Пример 2: скользящее окно на 10 точках с фиксированным размером обучения
Возьмём 10 последовательных наблюдений (индексы 0–9) и зафиксируем размер обучающего окна в 4 точки, тестовый блок — 1 точка, шаг — 1 точка вперёд. Получится:
-
Сплит 1: обучение — [0, 1, 2, 3], тест — [4].
-
Сплит 2: обучение — [1, 2, 3, 4], тест — [5].
-
Сплит 3: обучение — [2, 3, 4, 5], тест — [6].
-
Сплит 4: обучение — [3, 4, 5, 6], тест — [7].
-
Сплит 5: обучение — [4, 5, 6, 7], тест — [8].
-
Сплит 6: обучение — [5, 6, 7, 8], тест — [9].
Всего получилось $n - \text{train\_size} = 10 - 4 = 6$ сплитов. В отличие от расширяющегося окна, здесь обучающая выборка каждый раз имеет одинаковый размер (4 точки) и «уезжает» вперёд вместе с тестовым блоком — это особенно полезно, если старые данные быстро теряют актуальность (например, рынок или поведение пользователей заметно меняются со временем, и держать в обучении слишком старые наблюдения только вредит качеству).
Пример 3: чем оборачивается утечка из будущего на числах
Пусть на реальном временном ряде продаж честная кросс-валидация с расширяющимся окном (Пример 1) даёт средний MAPE (среднюю абсолютную процентную ошибку) 12%. Теперь применим к тем же данным обычный KFold(shuffle=True), не заботясь о хронологии: соседние по времени точки временного ряда обычно сильно коррелируют между собой (продажи в понедельник близки к продажам во вторник той же недели), поэтому даже при случайном перемешивании тестовые точки часто оказываются «временными соседями» каких-то обучающих точек — и модель отчасti использует эту близость как скрытую подсказку, которой не будет в реальном прогнозировании будущего. В таком нечестном сценарии средний MAPE по фолдам может составить, скажем, 5% — вдвое ниже, чем при честной схеме. Разница в 7 процентных пунктов — не заслуга более удачной модели, а чистая иллюзия, порождённая утечкой временной информации; в проде, где будущих данных по определению не существует на момент прогноза, реальная ошибка окажется куда ближе к честным 12%, чем к обманчивым 5%.
Почему это важно
Кросс-валидация для временных рядов — не факультативная тонкость, а единственный способ получить оценку качества, которая хоть сколько-нибудь соответствует тому, что произойдёт при реальном использовании модели. Обычный k-fold и тем более его стратифицированный вариант в задачах прогнозирования временных рядов не просто менее точны — они систематически завышают качество модели, потому что дают ей закамуфлированный доступ к информации из будущего. Правило здесь простое и жёсткое: если в данных есть время и есть зависимость от прошлого, перемешивание запрещено категорически, а разбиение всегда должно уважать хронологию.
Кросс-валидация для подбора гиперпараметров: cross_val_score и GridSearchCV
В прошлом уроке прозвучала важная мысль: если много раз подглядывать в один и тот же валидационный набор, подбирая по нему гиперпараметры, ты постепенно начинаешь подгонять модель под конкретный валидационный набор, а не под задачу в целом — итоговая оценка на валидации перестаёт быть честной именно потому, что валидация из независимого судьи превращается в ещё один источник обучающего сигнала. k-fold кросс-валидация — прямой практический ответ на эту проблему: вместо того чтобы подбирать гиперпараметр по одному фиксированному разбиению, ты подбираешь его по усреднённой оценке на нескольких разных разбиениях сразу — а значит, риск случайно подстроиться под особенности ровно одного конкретного валидационного набора резко снижается.
В sklearn.model_selection эта идея реализована в готовом виде. KFold (или StratifiedKFold, или TimeSeriesSplit — в зависимости от типа задачи) задаёт саму схему разбиения на фолды:
from sklearn.model_selection import KFold, cross_val_score
from sklearn.linear_model import Ridge
kf = KFold(n_splits=5, shuffle=True, random_state=42)
model = Ridge(alpha=1.0)
scores = cross_val_score(model, X, y, cv=kf, scoring="neg_mean_squared_error")
print(scores) # массив из 5 значений — по одному на фолд
print(scores.mean()) # усреднённая CV-оценка
print(scores.std()) # разброс оценки между фолдами
cross_val_score под капотом делает ровно то, что было разобрано в примерах выше: обучает модель $k$ раз, каждый раз на своей комбинации фолдов, и возвращает массив метрик — тебе остаётся только усреднить их и посмотреть на разброс.
Следующий шаг — перебор нескольких значений гиперпараметра с кросс-валидацией на каждом. Делать это вручную циклом можно, но GridSearchCV берёт эту рутину на себя целиком:
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestRegressor
param_grid = {
"max_depth": [3, 5, 7, 10],
"n_estimators": [50, 100, 200],
}
grid = GridSearchCV(
RandomForestRegressor(random_state=42),
param_grid=param_grid,
cv=5,
scoring="neg_mean_squared_error",
)
grid.fit(X_train, y_train)
print(grid.best_params_) # лучшая найденная комбинация гиперпараметров
print(grid.best_score_) # усреднённая CV-оценка на этой комбинации
Здесь сетка из 4 значений max_depth и 3 значений n_estimators даёт $4 \times 3 = 12$ комбинаций гиперпараметров, а при cv=5 для каждой комбинации обучается 5 моделей (по одной на фолд) — итого $12 \times 5 = 60$ обучений модели «под капотом» одного вызова grid.fit. GridSearchCV для каждой комбинации гиперпараметров считает усреднённую CV-оценку и в конце выбирает ту, у которой эта оценка лучшая, — а не ту, что случайно оказалась лучшей на одном-единственном валидационном наборе. По умолчанию (refit=True) после этого он ещё и автоматически переобучает финальную модель с лучшими параметрами уже на всём тренировочном наборе целиком — этой готовой моделью можно сразу пользоваться через grid.predict(...).
Важная деталь, о которой легко забыть: тестовая выборка, отложенная ещё на этапе train/test/validation-разбиения из прошлого урока, в этом процессе никак не участвует. GridSearchCV со своей внутренней кросс-валидацией работает только внутри тренировочной части — а финальную, ранее не тронутую тестовую выборку ты используешь ровно один раз, в самом конце, чтобы получить честную итоговую оценку уже выбранной модели. Кросс-валидация делает надёжнее процесс подбора гиперпараметров, но не отменяет необходимости в отдельном, никогда не использовавшемся для подбора тестовом наборе — это две разные защиты от двух разных рисков, и они дополняют, а не заменяют друг друга.
Практика: 30 заданий
Базовые задания (1–10)
Задание 1: Датасет содержит 20 объектов, применяется 5-fold кросс-валидация без остатка. Сколько объектов попадает в валидацию на каждом отдельном фолде?
Задание 2: Датасет содержит 23 объекта, $k=5$. Как разделятся объекты по 5 фолдам, если 23 не делится на 5 без остатка?
Задание 3: На датасете из 50 объектов применяется LOOCV. Сколько раз потребуется обучить модель?
Задание 4: На том же датасете из 50 объектов сравни число обучений модели при 5-fold CV и при LOOCV. Во сколько раз LOOCV требует больше вычислений?
Задание 5: Объясни своими словами: что именно усредняется в итоговой оценке k-fold кросс-валидации?
Задание 6: Датасет из 15 объектов с индексами 0–14, применяется KFold(n_splits=5, shuffle=False). Какие индексы попадут в валидацию третьего по счёту фолда?
Задание 7: На том же датасете из задания 6 — какие индексы попадут в валидацию пятого фолда, и в чём потенциальная проблема этого фолда, если объекты отсортированы по возрастанию значимого признака?
Задание 8: Почему обычная (нестратифицированная) k-fold кросс-валидация может дать плохую оценку качества на сильно несбалансированном датасете классов?
Задание 9: Почему для временного ряда нельзя применять k-fold кросс-валидацию со случайным перемешиванием (shuffle=True)?
Задание 10: Назови три инструмента sklearn.model_selection, применяющих идею кросс-валидации, упомянутые в этом уроке.
Продвинутые задания (11–20)
Задание 11: По пяти фолдам получены значения MSE: 0,12; 0,15; 0,11; 0,30; 0,13. Посчитай среднее и (выборочное) стандартное отклонение. О чём говорит результат?
Задание 12: Датасет из 100 объектов, классы в пропорции 90:10 (90 отрицательных, 10 положительных). При 5-fold разбиении каждый фолд содержит 20 объектов. Сколько положительных объектов ожидается в каждом фолде, если бы деление было идеально пропорциональным (как при стратификации)?
Задание 13: GridSearchCV перебирает сетку из 4 значений одного гиперпараметра и 3 значений другого, при cv=5. Сколько всего обучений модели произойдёт в процессе перебора (без учёта финального переобучения)?
Задание 14: Одно обучение модели занимает 3 секунды. GridSearchCV перебирает сетку из $5 \times 4 = 20$ комбинаций гиперпараметров при cv=10. Сколько времени займёт весь перебор?
Задание 15: Опиши схему расширяющегося окна для 20 точек временного ряда (индексы 0–19) при начальном обучении в 8 точек и тестовом блоке в 3 точки, до тех пор пока данных хватает.
Задание 16: Докажи, что в k-fold кросс-валидации каждый объект датасета попадает в валидацию ровно один раз, а в обучение — ровно $k-1$ раз.
Задание 17: Объясни, почему у LOOCV, несмотря на использование почти всех данных для обучения на каждой итерации, итоговая оценка не всегда оказывается более стабильной (менее изменчивой), чем у 5-fold или 10-fold CV.
Задание 18: Датасет из 15 объектов: 12 класса 0 и 3 класса 1. Как 3 объекта класса 1 распределятся по 5 фолдам при StratifiedKFold, если поровну (по 0,6 на фолд) распределить нельзя?
Задание 19: В чём принципиальная разница между расширяющимся (expanding) и скользящим (rolling) окном при кросс-валидации временных рядов? Приведи пример ситуации, где скользящее окно предпочтительнее.
Задание 20: Свяжи проблему одного фиксированного разбиения train/validation из прошлого урока с тем, зачем на маленьком датасете (скажем, 30 объектов) стоит использовать 10-fold CV, а не один train_test_split.
Продвинутые задания повышенной сложности (21–30)
Задание 21: Датасет из 12 объектов с индексами 0–11. Вручную выпиши все 4 фолда для KFold(n_splits=4, shuffle=False).
Задание 22: Временной ряд из 10 точек (индексы 0–9). Спроектируй скользящее окно с train_size=4, test_size=1, шагом 1 точка. Сколько всего сплитов получится, и как выглядит последний из них?
Задание 23: Объясни компромисс между смещением и вычислительной стоимостью при выборе $k$: почему на практике редко берут $k=2$ и редко берут $k=n$ (LOOCV), предпочитая значения вроде 5 или 10.
Задание 24: Ты уже использовал один валидационный набор для отбора признаков (из урока 305). Можно ли теперь на том же наборе подбирать гиперпараметры через кросс-валидацию и считать итоговую оценку честной? Опиши правильную последовательность действий.
Задание 25: В коде ниже есть ошибка, приводящая к утечке данных. Найди её и объясни, как исправить.
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import KFold, cross_val_score
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X) # масштабирование до разбиения на фолды
kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X_scaled, y, cv=kf)
Задание 26: 10-fold CV дал среднюю accuracy 0,87 со стандартным отклонением по фолдам 0,04. Оцени приближённо стандартную ошибку среднего и границы грубого 95%-интервала.
Задание 27: StratifiedKFold определён для классификации, где есть явные классы. Что можно сделать, чтобы получить похожую стратификацию для задачи регрессии, где целевая переменная непрерывна?
Задание 28: Опиши идею вложенной кросс-валидации (nested cross-validation) и объясни, зачем нужен именно двойной цикл — внешний и внутренний.
Задание 29: Во вложенной CV внешний цикл — cv=5, внутри каждого внешнего фолда работает GridSearchCV с cv=3 по сетке из 4 комбинаций гиперпараметров. Оцени общее число обучений модели, включая финальные переобучения лучшей комбинации внутри каждого внешнего фолда.
Задание 30: Датасет из 200 объектов с тремя классами в пропорции 150:40:10. Нужно подобрать гиперпараметр $C$ для SVM и получить честную итоговую оценку качества. Опиши весь план работы, связав его с материалом прошлого урока про train/validation/test.
Частые ошибки
Ошибка 1. Считают, что кросс-валидация полностью заменяет отдельную тестовую выборку.
Как выглядит: «раз я делаю 5-fold CV, отдельный тест уже не нужен — CV и так проверяет модель на всех данных».
Почему возникает: кажется, что раз каждая точка данных побывала в валидации, этого достаточно для честной итоговой оценки.
Как правильно: если CV использовалась для подбора гиперпараметров или отбора признаков, её оценка уже частично «подогнана» под конкретный датасет через процесс выбора; финальную, ничем не подпорченную оценку даёт только отдельная тестовая выборка, ни разу не участвовавшая в подборе.
Ошибка 2. Не перемешивают данные перед k-fold кросс-валидацией, если исходный порядок строк не случаен.
Как выглядит: используют KFold(n_splits=5) без shuffle=True на данных, отсортированных по дате, ID или значению признака.
Почему возникает: про порядок строк в датасете просто не задумываются, считая его несущественным.
Как правильно: всегда явно проверять, не отсортированы ли данные каким-либо неслучайным образом, и включать shuffle=True (с фиксированным random_state для воспроизводимости), если данные — не временной ряд.
Ошибка 3. Применяют обычный KFold вместо StratifiedKFold на задачах классификации с несбалансированными классами.
Как выглядит: редкий класс случайно концентрируется в одном-двух фолдах, а метрика вроде recall или F1 становится нестабильной или неопределённой на отдельных фолдах.
Почему возникает: по умолчанию многие используют KFold, не задумываясь о специализированном инструменте для классификации.
Как правильно: для любой задачи классификации, особенно с дисбалансом классов, по умолчанию использовать StratifiedKFold.
Ошибка 4. Применяют обычную (не привязанную ко времени) кросс-валидацию к временным рядам.
Как выглядит: KFold(shuffle=True) или даже KFold(shuffle=False) без учёта хронологии на данных с явной временной структурой.
Почему возникает: временной ряд формально выглядит как обычная таблица строк, и специфика зависимости от времени не бросается в глаза.
Как правильно: использовать TimeSeriesSplit или вручную реализованную схему расширяющегося/скользящего окна, при которой обучение всегда предшествует валидации по времени.
Ошибка 5. Выполняют препроцессинг данных (масштабирование, отбор признаков, кодирование категорий) до разбиения на фолды, а не внутри каждого фолда отдельно.
Как выглядит: StandardScaler().fit_transform(X) применяется к полному датасету перед вызовом cross_val_score или GridSearchCV.
Почему возникает: кажется, что масштабирование — нейтральная техническая операция, не влияющая на честность оценки.
Как правильно: оборачивать весь препроцессинг вместе с моделью в sklearn.pipeline.Pipeline и передавать этот пайплайн целиком в cross_val_score/GridSearchCV, чтобы статистики препроцессинга пересчитывались заново на обучающей части каждого отдельного фолда.
Ошибка 6. Считают, что чем больше $k$, тем однозначно лучше оценка, не учитывая вычислительную цену и возможный рост изменчивости при экстремальных значениях $k$.
Как выглядит: по умолчанию выбирают LOOCV «для максимальной точности» на датасете из десятков тысяч строк.
Почему возникает: интуитивно кажется, что использование почти всех данных для обучения на каждой итерации не может дать оценку хуже.
Как правильно: учитывать выбор $k$ как компромисс между смещением оценки, её изменчивостью и вычислительной ценой; для большинства задач $k=5$ или $k=10$ — разумная отправная точка, а LOOCV стоит держать в резерве для действительно маленьких датасетов.
Ошибка 7. Путают cross_val_score (получение честной оценки качества) с механизмом, автоматически подбирающим итоговую модель.
Как выглядит: после cross_val_score пытаются понять, «какую из пяти обученных на фолдах моделей теперь использовать в проде».
Почему возникает: неверное понимание роли кросс-валидации — она даёт оценку качества подхода в целом, а не выбирает одну конкретную модель из числа обученных на фолдах.
Как правильно: после того как кросс-валидация подтвердила качество подхода (или после того как GridSearchCV выбрал лучшие гиперпараметры), финальную модель переобучают на всей доступной тренировочной выборке целиком — модели, обученные на отдельных фолдах, для продакшена не используются.
Главное запомнить
-
Кросс-валидация решает проблему одного случайного разбиения train/validation: вместо единственной оценки качества на единственном валидационном наборе она даёт несколько оценок на разных наборах и усредняет их.
-
k-fold кросс-валидация делит данные на $k$ примерно равных фолдов и по очереди делает каждый из них валидационным, а остальные $k-1$ — обучающими; итоговая оценка — среднее по всем $k$ прогонам.
-
Типичные значения $k$ — 5 или 10; это эмпирически подобранный компромисс между точностью оценки и вычислительной стоимостью, закреплённый, в частности, исследованием Рона Кохави 1995 года.
-
Leave-one-out кросс-валидация (LOOCV) — частный случай k-fold при $k=n$: на каждой итерации валидируется ровно один объект. Она почти несмещена, но вычислительно дорога и не всегда даёт более стабильную оценку из-за сильной корреляции между итерациями.
-
Стратифицированная k-fold кросс-валидация (
StratifiedKFold) сохраняет пропорцию классов в каждом фолде — обязательна для классификации с несбалансированными классами. -
Для временных рядов обычный k-fold запрещён из-за риска утечки данных из будущего; вместо него используется схема с расширяющимся или скользящим окном (
TimeSeriesSplit), где обучение всегда предшествует валидации по времени. -
Больше фолдов дают более точную (менее зависимую от одной случайности) оценку качества, но требуют пропорционально больше вычислений — этот компромисс нужно осознанно выбирать под конкретную задачу и бюджет времени.
-
cross_val_scoreсчитает метрику качества по схеме кросс-валидации, аGridSearchCVиспользует ту же идею для перебора и честного сравнения гиперпараметров, избегая переподгонки под единственный фиксированный валидационный набор. -
Кросс-валидация не заменяет отдельную тестовую выборку: она делает надёжнее процесс отбора признаков и подбора гиперпараметров на тренировочных данных, но честную итоговую оценку по-прежнему даёт только один финальный прогон на ранее не тронутом тесте.
-
Весь препроцессинг данных должен пересчитываться отдельно на обучающей части каждого фолда — для этого препроцессинг и модель оборачивают в единый
Pipeline, а не применяют препроцессинг ко всему датасету заранее.
Связь с темами курса
Что нужно было знать до этого урока
Урок 305 показал, как данные делятся на тренировочную, валидационную и тестовую части, и почему валидацию нельзя использовать многократно для подбора решений без последствий — это постепенно превращает её из независимого судьи в ещё один источник переподгонки. Урок 306 отвечает на прямое продолжение этого вопроса: если валидацию нельзя переиспользовать бесконечно, а одно фиксированное разбиение к тому же даёт неустойчивую оценку, — что делать? Кросс-валидация решает обе проблемы сразу: вместо одного фиксированного валидационного набора она использует несколько разных «версий» разбиения, усредняя оценку и тем самым делая её устойчивее к случайности конкретного разбиения, а заодно даёт более надёжный инструмент для честного перебора гиперпараметров, чем повторяющееся подглядывание в один и тот же кусок данных.
Что изучить дальше
Следующий урок 307 переходит к метрикам качества — тому, что именно ты усредняешь по фолдам в кросс-валидации. Пока в примерах этого урока метрики (MSE, recall, accuracy) использовались как данность, но у каждой из них — свои сильные и слабые стороны, свои ловушки на несбалансированных данных и своя область применимости. Понимание метрик качества — необходимое дополнение к пройденной сегодня технике: кросс-валидация даёт надёжный процесс оценивания, но выбор правильной метрики внутри этого процесса определяет, оцениваешь ли ты вообще то, что нужно оценивать.
Где это нужно в жизни
📊 Соревнования по машинному обучению (Kaggle и аналоги). Практически все топовые решения строятся на многократной, тщательно организованной кросс-валидации — она позволяет надёжно сравнивать модели и признаки без доступа к скрытому тестовому набору соревнования.
🏦 Скоринговые и антифрод-модели. Здесь классы почти всегда сильно разбалансированы (мошенничество и дефолты редки), и стратифицированная кросс-валидация — стандартный инструмент получения надёжных оценок recall и precision.
📈 Прогнозирование спроса и финансовых показателей. Кросс-валидация с расширяющимся или скользящим окном — обязательная часть пайплайна для любой модели, работающей с временными рядами продаж, трафика или котировок.
⚙️ Автоматический подбор гиперпараметров в продакшен-пайплайнах. GridSearchCV и его более продвинутые аналоги (RandomizedSearchCV, байесовская оптимизация гиперпараметров из уроков про оптимизацию) в промышленных ML-системах почти всегда используют кросс-валидацию как внутренний механизм честной оценки каждой пробуемой конфигурации.
Интересные факты
-
Идея, лежащая в основе leave-one-out кросс-валидации, восходит к методу «складного ножа» (jackknife), предложенному Морисом Кенуем в 1949 году, — задолго до того, как кто-либо использовал термин «машинное обучение» в его нынешнем смысле.
-
Для обычной линейной регрессии существует изящный математический трюк (формула PRESS, predicted residual error sum of squares), позволяющий вычислить результат LOOCV без реального повторного обучения модели $n$ раз — достаточно один раз обучить модель на всех данных и воспользоваться диагональю так называемой hat-матрицы (матрицы «шляпы», проецирующей вектор наблюдений на предсказания). Для большинства других моделей такого бесплатного трюка, увы, не существует.
-
Термин «cross-validation» в его современном значении закрепился благодаря статьям Мервина Стоуна и Сеймура Гайссера 1974–1975 годов, а не одному конкретному автору — это относительно редкий случай, когда термин прочно вошёл в обиход почти одновременно из двух независимых источников.
-
Значение $k=10$, которое сегодня стоит по умолчанию во множестве библиотек и туториалов, — не произвольное «круглое число», а результат конкретного эмпирического исследования: статьи Рона Кохави 1995 года, сравнившей разные $k$ на целом ряде реальных датасетов и алгоритмов.
Лайфхаки
-
Начинай с $k=5$ или $k=10$ как с разумного значения по умолчанию — оба варианта хорошо изучены на практике и обычно дают устойчивый баланс между качеством оценки и временем вычислений; между ними выбирай по бюджету времени, а не по интуиции.
-
На очень маленьких датасетах (условно, меньше 100 объектов) склоняйся к большему $k$, вплоть до LOOCV, — там каждая обучающая строка ценна, и жертвовать 20% данных ради валидации, как при одном фиксированном разбиении, особенно болезненно.
-
На больших датасетах и/или с дорогими в обучении моделями (градиентный бустинг с большим числом деревьев, нейросети) снижай $k$ до 3 или используй один честно устроенный hold-out split — вычислительный бюджет там часто важнее выигрыша в точности оценки от дополнительных фолдов.
-
Для любой задачи классификации по умолчанию бери
StratifiedKFold, а неKFold, — почти никогда не бывает хуже, а при малейшем дисбалансе классов эта разница становится критической. -
Для временных рядов никогда не включай
shuffle=Trueи не используй обычныйKFold— сразу берись заTimeSeriesSplitили вручную спроектированную схему окна, даже если данные «на глаз» не выглядят как классический временной ряд. -
Оборачивай весь препроцессинг и модель в единый
sklearn.pipeline.Pipeline, прежде чем передавать их вcross_val_scoreилиGridSearchCV, — это не только защищает от утечки данных, но и делает код короче и надёжнее. -
Всегда смотри не только на среднее значение метрики по фолдам, но и на стандартное отклонение — большой разброс сигнализирует либо о маленьком/шумном датасете, либо о неудачно спроектированной схеме разбиения (проверь, не забыл ли ты про перемешивание или стратификацию).
-
Задавай
n_jobs=-1вcross_val_scoreиGridSearchCV, если библиотека и окружение это позволяют, — обучение на разных фолдах и разных комбинациях гиперпараметров независимо друг от друга, и параллельное выполнение может заметно ускорить перебор.
Кросс-валидация — не экзотическая техника «для профессионалов», а рабочая гигиена, которая отделяет случайно удачное разбиение данных от реального качества модели. Освоив её сегодня, ты закрыл главный практический пробел прошлого урока: теперь у тебя есть инструмент, который честно подбирает гиперпараметры через GridSearchCV, устойчиво оценивает качество на маленьких и несбалансированных датасетах и не даёт себя обмануть утечкой данных из будущего во временных рядах. В следующем уроке ты разберёшь, какими именно метриками измерять то самое качество, которое ты теперь умеешь оценивать так надёжно.
Понял тему? Закрепи в боте! 🚀
Попрактикуйся на задачах и получи персональные рекомендации от AI
💪 Начать тренировку