← Назад к вопросам
Как проверял тестами работу моделей\?
1.7 Middle🔥 161 комментариев
#MLOps и инфраструктура#Опыт и проекты
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Тестирование ML моделей: полная стратегия
Тестирование моделей сильно отличается от классического unit-тестирования. Невозможно знать идеальный ответ заранее, поэтому используется вероятностная валидация и статистические тесты.
Уровень 1: Unit тесты для preprocessing
Проверяем, что данные подготавливаются корректно:
import pytest
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
# Тестируем подготовку данных
class TestDataPreprocessing:
def test_missing_values_handling(self):
"""Проверяем, что пропуски заполняются корректно"""
data = pd.DataFrame({
'age': [25, np.nan, 35, 40],
'income': [50000, 60000, np.nan, 80000]
})
# Заполняем медианой
data_filled = data.fillna(data.median())
assert data_filled['age'].isna().sum() == 0
assert data_filled['income'].isna().sum() == 0
assert data_filled['age'][1] == 32.5 # медиана
def test_scaling_transforms_correctly(self):
"""Проверяем нормализацию"""
X = np.array([[1, 2], [3, 4], [5, 6]])
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Проверяем, что масштабирование работает
assert X_scaled.mean() < 0.01 # близко к 0
assert X_scaled.std() < 1.01 # близко к 1
def test_categorical_encoding(self):
"""Проверяем кодирование категориальных признаков"""
data = pd.DataFrame({
'category': ['A', 'B', 'A', 'C']
})
encoded = pd.get_dummies(data, columns=['category'])
assert 'category_A' in encoded.columns
assert 'category_B' in encoded.columns
assert len(encoded) == 4
assert encoded['category_A'].sum() == 2
def test_no_data_leakage(self):
"""Проверяем, что нет утечки данных из test в train"""
from sklearn.model_selection import train_test_split
X = np.random.randn(100, 10)
y = np.random.randint(0, 2, 100)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Нет пересечений
assert len(X_train) == 80
assert len(X_test) == 20
assert len(np.intersect1d(X_train, X_test)) == 0
Уровень 2: Integration тесты (обучение-предсказание)
Проверяем, что модель учится и даёт разумные предсказания:
class TestModelTraining:
def test_model_overfitting_detection(self):
"""Проверяем, что модель учится (обучающая точность > тестовая)"""
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
train_acc = accuracy_score(y_train, model.predict(X_train))
test_acc = accuracy_score(y_test, model.predict(X_test))
# Модель должна переобучаться немного (train > test)
assert train_acc > test_acc
# Но тестовая должна быть разумной
assert test_acc > 0.7
def test_predictions_in_valid_range(self):
"""Проверяем, что предсказания в корректном диапазоне"""
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_breast_cancer
X, y = load_breast_cancer(return_X_y=True)
model = LogisticRegression(max_iter=1000)
model.fit(X, y)
y_proba = model.predict_proba(X)
# Вероятности должны быть в [0, 1]
assert np.all(y_proba >= 0)
assert np.all(y_proba <= 1)
# Сумма вероятностей = 1
assert np.allclose(y_proba.sum(axis=1), 1.0)
def test_model_reproducibility(self):
"""Проверяем, что модель воспроизводима"""
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
# Первый запуск
model1 = GradientBoostingClassifier(random_state=42)
model1.fit(X, y)
pred1 = model1.predict(X)
# Второй запуск с тем же random_state
model2 = GradientBoostingClassifier(random_state=42)
model2.fit(X, y)
pred2 = model2.predict(X)
# Результаты должны быть идентичны
assert np.array_equal(pred1, pred2)
Уровень 3: Метрики и производительность
Проверяем, что модель достигает целевых метрик:
class TestModelMetrics:
def test_model_meets_business_requirements(self):
"""Проверяем, что метрики соответствуют бизнес-требованиям"""
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import recall_score, precision_score, roc_auc_score
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
y_proba = model.predict_proba(X_test)[:, 1]
# Требование: recall >= 0.90 (ловим 90% болезней)
recall = recall_score(y_test, y_pred)
assert recall >= 0.90, f"Recall {recall:.2f} ниже требуемого 0.90"
# Требование: precision >= 0.85
precision = precision_score(y_test, y_pred)
assert precision >= 0.85, f"Precision {precision:.2f} ниже требуемого 0.85"
# Требование: ROC-AUC >= 0.95
roc_auc = roc_auc_score(y_test, y_proba)
assert roc_auc >= 0.95, f"ROC-AUC {roc_auc:.2f} ниже требуемого 0.95"
def test_baseline_comparison(self):
"""Проверяем, что модель лучше baseline"""
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.datasets import load_iris
from sklearn.model_selection import cross_val_score
X, y = load_iris(return_X_y=True)
# Baseline: логистическая регрессия
baseline = LogisticRegression(max_iter=1000)
baseline_score = cross_val_score(
baseline, X, y, cv=5, scoring='roc_auc'
).mean()
# Предложенная модель
model = RandomForestClassifier(n_estimators=100, random_state=42)
model_score = cross_val_score(
model, X, y, cv=5, scoring='roc_auc'
).mean()
# Должна быть лучше baseline
assert model_score > baseline_score, \
f"Model ROC-AUC {model_score:.3f} <= Baseline {baseline_score:.3f}"
def test_no_data_leakage_in_validation(self):
"""Проверяем отсутствие утечки при кросс-валидации"""
from sklearn.model_selection import StratifiedKFold
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
train_indices_list = []
test_indices_list = []
for train_idx, test_idx in kfold.split(X, y):
train_indices_list.append(set(train_idx))
test_indices_list.append(set(test_idx))
# Проверяем, что нет пересечений
assert len(train_idx) + len(test_idx) == len(X)
assert len(set(train_idx) & set(test_idx)) == 0
Уровень 4: Статистические тесты
Проверяем значимость результатов:
from scipy import stats
class TestStatisticalSignificance:
def test_cross_validation_stability(self):
"""Проверяем, что результаты кросс-валидации стабильны"""
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
model = RandomForestClassifier(n_estimators=100, random_state=42)
scores = cross_val_score(model, X, y, cv=10, scoring='accuracy')
# Результаты должны быть стабильны (низкий std)
assert scores.std() < 0.1, f"Нестабильные результаты: std={scores.std():.3f}"
# Среднее значение > 0.9
assert scores.mean() > 0.9, f"Низкая точность: mean={scores.mean():.3f}"
def test_significance_of_improvement(self):
"""Проверяем статистическую значимость улучшения"""
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
# Две модели
rf = RandomForestClassifier(n_estimators=100, random_state=42)
gb = GradientBoostingClassifier(random_state=42)
# Кросс-валидация
scores_rf = cross_val_score(rf, X, y, cv=10, scoring='accuracy')
scores_gb = cross_val_score(gb, X, y, cv=10, scoring='accuracy')
# t-тест (значимо ли различие?)
t_stat, p_value = stats.ttest_ind(scores_gb, scores_rf)
# p < 0.05 означает значимое различие
# Если различие есть, то модель действительно лучше
if p_value < 0.05:
assert scores_gb.mean() > scores_rf.mean()
Уровень 5: Regression тесты и мониторинг
Проверяем, что модель не деградирует со временем:
class TestModelDegradation:
def test_model_performance_on_new_data(self):
"""Проверяем, что модель работает на новых данных"""
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
X, y = load_iris(return_X_y=True)
# Тренируем на старых данных
X_train, X_old_test, y_train, y_old_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
old_auc = roc_auc_score(y_old_test, model.predict_proba(X_old_test)[:, 1])
# Проверяем на новых данных (симулируем)
X_new_test, y_new_test = load_iris(return_X_y=True)
X_train, X_new_test, y_train, y_new_test = train_test_split(
X_new_test, y_new_test, test_size=0.2, random_state=43
)
new_auc = roc_auc_score(y_new_test, model.predict_proba(X_new_test)[:, 1])
# Проверяем, что деградация не слишком сильная
degradation = old_auc - new_auc
assert degradation < 0.1, \
f"Слишком сильная деградация: {degradation:.3f}"
def test_feature_importance_stability(self):
"""Проверяем стабильность важности признаков"""
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
# Тренируем две модели с разными random_state
model1 = RandomForestClassifier(n_estimators=100, random_state=42)
model1.fit(X_train, y_train)
importance1 = model1.feature_importances_
model2 = RandomForestClassifier(n_estimators=100, random_state=43)
model2.fit(X_train, y_train)
importance2 = model2.feature_importances_
# Корреляция важности признаков должна быть высокой
correlation = np.corrcoef(importance1, importance2)[0, 1]
assert correlation > 0.8, \
f"Нестабильная важность признаков: корреляция={correlation:.3f}"
Полный пример с pytest
# conftest.py - фикстуры
import pytest
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
@pytest.fixture
def iris_data():
"""Фикстура для загрузки Iris датасета"""
X, y = load_iris(return_X_y=True)
return train_test_split(X, y, test_size=0.3, random_state=42)
@pytest.fixture
def trained_model(iris_data):
"""Фикстура для обученной модели"""
from sklearn.ensemble import RandomForestClassifier
X_train, X_test, y_train, y_test = iris_data
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
return model, X_test, y_test
# Использование
def test_model_with_fixture(trained_model):
model, X_test, y_test = trained_model
assert model.score(X_test, y_test) > 0.8
Запуск тестов
# Все тесты
pytest tests/ -v
# Тесты с покрытием
pytest tests/ --cov=src --cov-report=html
# Только медленные тесты
pytest tests/ -m slow
# Параллельно
pytest tests/ -n auto
# С вывод всех print
pytest tests/ -s
Ключевые принципы
- Unit тесты: логика обработки данных
- Integration тесты: обучение и предсказание
- Метрики: соответствие бизнес-требованиям
- Статистика: значимость результатов
- Мониторинг: отсутствие деградации
- Воспроизводимость: контроль random_state
- Data leakage: проверка отсутствия утечек