← Назад к вопросам
Как сплитуешь данные для избежания переобучения?
1.0 Junior🔥 241 комментариев
#Машинное обучение
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Стратегии разделения данных для избежания переобучения
Правильное разделение данных (splitting strategy) — это фундамент надёжного машинного обучения. Расскажу о всех ключевых подходах и когда каждый применяется.
Базовый train-test split
from sklearn.model_selection import train_test_split
import numpy as np
import pandas as pd
# Самый простой случай
X = np.random.randn(1000, 10)
y = np.random.rand(1000)
# Случайное разделение 80% train, 20% test
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42 # Для воспроизводимости!
)
print(f"Train: {X_train.shape[0]}, Test: {X_test.shape[0]}")
# Train: 800, Test: 200
# ВАЖНО: test_size обычно 0.2, но для больших данных может быть меньше
# Для 1M строк можно использовать 0.05 (50K тестовых)
Проблема: Один test split может быть "везучим". Решение: кросс-валидация.
K-Fold кросс-валидация
Разделяем данные на K частей и K раз тренируем модель:
from sklearn.model_selection import KFold, cross_val_score
from sklearn.ensemble import RandomForestClassifier
# 5-fold кросс-валидация
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
model = RandomForestClassifier(random_state=42)
scores = cross_val_score(model, X, y, cv=kfold, scoring='accuracy')
print(f"Scores каждого fold: {scores}")
print(f"Mean: {scores.mean():.4f}, Std: {scores.std():.4f}")
# Результат:
# Scores: [0.89, 0.91, 0.88, 0.90, 0.92]
# Mean: 0.9000, Std: 0.0139
# Стандартное отклонение показывает стабильность оценки
Когда использовать:
- K=5 или K=10 — стандарт для большинства задач
- K=3 — если данных мало
- K=n (leave-one-out) — только для очень маленьких выборок (< 100 строк)
Stratified K-Fold для классификации
Если целевая переменная несбалансирована, используем stratified:
from sklearn.model_selection import StratifiedKFold
# Несбалансированные данные
y_imbalanced = np.array([0]*950 + [1]*50) # 95% класс 0, 5% класс 1
print(f"Процент класса 1: {y_imbalanced.mean()*100:.1f}%")
# Обычный KFold может создать fold, где класс 1 вообще отсутствует
kfold = KFold(n_splits=5, shuffle=True)
for train_idx, test_idx in kfold.split(X, y_imbalanced):
print(f"Класс 1 в train: {y_imbalanced[train_idx].mean()*100:.1f}%")
print(f"Класс 1 в test: {y_imbalanced[test_idx].mean()*100:.1f}%")
# Результат может быть:
# Класс 1 в train: 3.8%
# Класс 1 в test: 10.0% ← Несогласованно!
# Stratified KFold сохраняет распределение
skfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for train_idx, test_idx in skfold.split(X, y_imbalanced):
print(f"Класс 1 в train: {y_imbalanced[train_idx].mean()*100:.1f}%")
print(f"Класс 1 в test: {y_imbalanced[test_idx].mean()*100:.1f}%")
# Результат:
# Класс 1 в train: 5.0%
# Класс 1 в test: 5.0% ← Консистентно!
Time Series Split
Для временных рядов обычный random split — ошибка (data leakage):
from sklearn.model_selection import TimeSeriesSplit
import matplotlib.pyplot as plt
# Временные данные
X_time = np.random.randn(1000, 5)
y_time = np.cumsum(np.random.randn(1000)) # Случайный walk
# НЕПРАВИЛЬНО: случайное разделение
# X_train, X_test = X_time[:800], X_time[800:]
# Модель видит будущие данные во время обучения!
# ПРАВИЛЬНО: TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
for fold, (train_idx, test_idx) in enumerate(tscv.split(X_time)):
print(f"Fold {fold}:")
print(f" Train: индексы {train_idx[0]} to {train_idx[-1]}")
print(f" Test: индексы {test_idx[0]} to {test_idx[-1]}")
# Результат:
# Fold 0:
# Train: индексы 0 to 199
# Test: индексы 200 to 249
# Fold 1:
# Train: индексы 0 to 249
# Test: индексы 250 to 299
# ... и так далее
# Визуализация
fig, ax = plt.subplots()
for fold, (train_idx, test_idx) in enumerate(tscv.split(X_time)):
ax.scatter(train_idx, [fold]*len(train_idx), c='blue', marker='s', label='train' if fold == 0 else '')
ax.scatter(test_idx, [fold]*len(test_idx), c='red', marker='s', label='test' if fold == 0 else '')
ax.set_xlabel('Time index')
ax.set_ylabel('Fold')
plt.legend()
plt.show()
GroupKFold для зависимых данных
Если данные сгруппированы (например, несколько записей от одного пользователя), нужна GroupKFold:
from sklearn.model_selection import GroupKFold
# Данные: несколько предложений от каждого пользователя
user_ids = np.array([1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5])
X_grouped = np.random.randn(15, 10)
y_grouped = np.random.rand(15)
print(f"Уникальных пользователей: {len(np.unique(user_ids))}")
# НЕПРАВИЛЬНО: использовать обычный KFold
kfold = KFold(n_splits=3, shuffle=True)
for train_idx, test_idx in kfold.split(X_grouped):
print(f"Train users: {np.unique(user_ids[train_idx])}")
print(f"Test users: {np.unique(user_ids[test_idx])}")
# Может быть утечка: пользователь в обучении и тесте одновременно!
# ПРАВИЛЬНО: GroupKFold
groupkfold = GroupKFold(n_splits=3)
for train_idx, test_idx in groupkfold.split(X_grouped, y_grouped, groups=user_ids):
print(f"Train users: {np.unique(user_ids[train_idx])}")
print(f"Test users: {np.unique(user_ids[test_idx])}")
# Результат:
# Train users: [1 2 3]
# Test users: [4 5]
# Нет утечки!
Вложенная (nested) кросс-валидация
Для выбора гиперпараметров без смещения оценки:
from sklearn.model_selection import cross_validate, GridSearchCV
from sklearn.svm import SVC
# Внутренний loop: подбор гиперпараметров
# Внешний loop: оценка обобщаемости
inner_cv = KFold(n_splits=5, shuffle=True, random_state=42)
outer_cv = KFold(n_splits=3, shuffle=True, random_state=42)
# GridSearchCV проводит внутреннюю кросс-валидацию
gsearch = GridSearchCV(
SVC(),
param_grid={'C': [0.1, 1, 10]},
cv=inner_cv
)
# cross_validate проводит внешнюю кросс-валидацию
scores = cross_validate(gsearch, X, y, cv=outer_cv, scoring='accuracy')
print(f"Outer CV scores: {scores['test_score']}")
print(f"Средняя оценка: {scores['test_score'].mean():.4f}")
print(f"Это реальная оценка обобщаемости без смещения!")
Практический пример: избежание утечек данных
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
# НЕПРАВИЛЬНО: масштабируем ДО разделения
X_scaled = StandardScaler().fit_transform(X) # Информация из test попадает в train!
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2)
# ПРАВИЛЬНО: масштабируем ПОСЛЕ разделения
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # Подгонка на train!
X_test_scaled = scaler.transform(X_test) # Применение на test
# ИЛИ используем Pipeline (автоматизирует правильный порядок)
pipeline = Pipeline([
('scaler', StandardScaler()),
('model', LogisticRegression())
])
# Кросс-валидация гарантирует правильный порядок
scores = cross_val_score(pipeline, X, y, cv=5)
Размер train/test split
# Зависит от размера данных и сложности модели
data_sizes = {
'< 1000': {'train': 0.7, 'val': 0.15, 'test': 0.15},
'1K-10K': {'train': 0.8, 'val': 0.1, 'test': 0.1},
'10K-1M': {'train': 0.8, 'val': 0.1, 'test': 0.1},
'> 1M': {'train': 0.95, 'val': 0.02, 'test': 0.03}
}
# Для 100K строк и простой модели:
# train: 80K, validation: 10K, test: 10K
# Достаточно для надёжной оценки
Три-split стратегия (train/val/test)
from sklearn.model_selection import train_test_split
# Шаг 1: разделить на train (80%) и temp (20%)
X_train, X_temp, y_train, y_temp = train_test_split(
X, y,
test_size=0.2,
random_state=42
)
# Шаг 2: разделить temp на val (50%) и test (50%)
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp,
test_size=0.5,
random_state=42
)
print(f"Train: {len(X_train)} ({len(X_train)/len(X)*100:.1f}%)")
print(f"Val: {len(X_val)} ({len(X_val)/len(X)*100:.1f}%)")
print(f"Test: {len(X_test)} ({len(X_test)/len(X)*100:.1f}%)")
# Train: 800 (80.0%)
# Val: 100 (10.0%)
# Test: 100 (10.0%)
# Использование:
# 1. Train: обучение модели
# 2. Val: выбор гиперпараметров, early stopping
# 3. Test: финальная оценка (не используется при обучении!)
Чек-лист для правильного splitting
print("Чек-лист избежания переобучения через правильный split:")
print()
print("✓ Используй кросс-валидацию, а не одиночный train-test split")
print("✓ Используй StratifiedKFold для классификации с дисбалансом")
print("✓ Используй TimeSeriesSplit для временных рядов")
print("✓ Используй GroupKFold если данные зависимы")
print("✓ Предобработка (scaling, encoding) делается ПОСЛЕ split")
print("✓ Гиперпараметры подбираются на валидационном наборе")
print("✓ Финальная оценка только на тестовом наборе")
print("✓ random_state зафиксирован для воспроизводимости")
print("✓ Проверь размеры train/val/test (обычно 70/15/15 или 80/10/10)")
print("✓ Для временных рядов НЕ используй случайное разделение")
print()
print("Результат: модель, которая действительно обобщается на новые данные!")
Инструмент для автоматизации
def create_cv_splits(X, y, groups=None, task='classification', time_series=False):
"""
Выбирает правильный CV stratego автоматически
"""
if time_series:
return TimeSeriesSplit(n_splits=5)
if groups is not None:
return GroupKFold(n_splits=5)
if task == 'classification':
return StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
else:
return KFold(n_splits=5, shuffle=True, random_state=42)
# Использование
cv = create_cv_splits(X, y, task='classification')
scores = cross_val_score(model, X, y, cv=cv)
Выводы
Ключевые правила:
- Всегда используй кросс-валидацию для надёжной оценки
- Выбор CV стратегии зависит от данных (StratifiedKFold, TimeSeriesSplit, GroupKFold)
- Предобработка ПОСЛЕ разделения (масштабирование, кодирование)
- Гиперпараметры подбираются на валидации (никогда на test!)
- random_state для воспроизводимости
- Три набора (train/val/test) если есть ранняя остановка
Правильное разделение — это 50% успеха в предотвращении переобучения!