← Назад к вопросам
Что такое дисбаланс классов и как с ним бороться?
2.3 Middle🔥 221 комментариев
#Машинное обучение#Метрики и оценка моделей
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Что такое дисбаланс классов и как с ним бороться?
Дисбаланс классов (Class Imbalance) — это ситуация, когда в датасете количество примеров одного класса намного больше, чем другого. Например, 95% примеров класса "нормально", 5% класса "аномалия". Это приводит к предвзятости модели в сторону большинства класса.
Проблемы дисбаланса
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix, classification_report, roc_auc_score
# Создаём несбалансированный датасет
X, y = make_classification(
n_samples=1000,
n_features=20,
weights=[0.95, 0.05], # 95% класс 0, 5% класс 1
random_state=42
)
print(f"Распределение классов:")
print(f"Класс 0: {sum(y==0)} примеров ({sum(y==0)/len(y)*100:.1f}%)")
print(f"Класс 1: {sum(y==1)} примеров ({sum(y==1)/len(y)*100:.1f}%)")
# Наивная модель (просто предсказывает "0")
naive_model = lambda x: np.zeros(len(x))
accuracy_naive = (y == 0).mean() # 95% точность!
print(f"\nНаивная модель (всегда класс 0): Accuracy = {accuracy_naive:.4f}")
print(f"Но Recall для класса 1 = 0 (пропускаем все аномалии!)")
Метрики для несбалансированных данных
from sklearn.metrics import (
confusion_matrix, classification_report, roc_auc_score,
f1_score, precision_recall_curve, average_precision_score
)
model = LogisticRegression(random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
y_pred_proba = model.predict_proba(X_test)[:, 1]
print("\nОЧЕНЬ НЕПРАВИЛЬНО (игнорирует дисбаланс):")
print(f"Accuracy: {(y_test == y_pred).mean():.4f}")
print(f"Это может быть высокая, но модель бесполезна!")
print("\nПРАВИЛЬНО (для несбалансированных данных):")
print(f"ROC-AUC: {roc_auc_score(y_test, y_pred_proba):.4f}")
print(f"F1-Score: {f1_score(y_test, y_pred):.4f}")
print(f"Precision: {precision_recall_curve(y_test, y_pred_proba)}")
print(f"Average Precision: {average_precision_score(y_test, y_pred_proba):.4f}")
print("\nСбалансированная точность:")
from sklearn.metrics import balanced_accuracy_score
print(f"Balanced Accuracy: {balanced_accuracy_score(y_test, y_pred):.4f}")
Методы борьбы с дисбалансом
1. Oversampling (увеличение меньшинства)
from imblearn.over_sampling import RandomOverSampler, SMOTE
print(f"\nИсходный датасет:")
print(f"Класс 0: {sum(y==0)}, Класс 1: {sum(y==1)}")
# Случайный oversampling
ros = RandomOverSampler(random_state=42)
X_oversampled, y_oversampled = ros.fit_resample(X_train, y_train)
print(f"\nПосле Random Oversampling:")
print(f"Класс 0: {sum(y_oversampled==0)}, Класс 1: {sum(y_oversampled==1)}")
# SMOTE (Synthetic Minority Over-sampling TEchnique)
# Создаёт синтетические примеры, а не просто копирует
smote = SMOTE(random_state=42)
X_smote, y_smote = smote.fit_resample(X_train, y_train)
print(f"\nПосле SMOTE:")
print(f"Класс 0: {sum(y_smote==0)}, Класс 1: {sum(y_smote==1)}")
print(f"SMOTE создаёт реалистичные примеры между соседями")
# Обучаем модель на перебалансированных данных
model = LogisticRegression(random_state=42)
model.fit(X_smote, y_smote)
2. Undersampling (уменьшение большинства)
from imblearn.under_sampling import RandomUnderSampler, TomekLinks
# Случайный undersampling
rus = RandomUnderSampler(random_state=42)
X_undersampled, y_undersampled = rus.fit_resample(X_train, y_train)
print(f"\nПосле Random Undersampling:")
print(f"Класс 0: {sum(y_undersampled==0)}, Класс 1: {sum(y_undersampled==1)}")
print(f"МИНУС: теряем данные из большинства класса")
# TomekLinks: удаляет только противоречивые примеры
tomek = TomekLinks()
X_tomek, y_tomek = tomek.fit_resample(X_train, y_train)
print(f"\nПосле TomekLinks:")
print(f"Удалено противоречивых примеров: {len(X_train) - len(X_tomek)}")
3. Комбинированные методы (SMOTE + Tomek)
from imblearn.pipeline import Pipeline
from imblearn.over_sampling import SMOTE
from imblearn.under_sampling import TomekLinks
# Pipeline: сначала SMOTE, потом TomekLinks
pipeline = Pipeline([
('smote', SMOTE(random_state=42)),
('tomek', TomekLinks())
])
X_combined, y_combined = pipeline.fit_resample(X_train, y_train)
print(f"Комбинированный подход: {len(X_combined)} примеров")
4. Регулировка весов классов
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.utils.class_weight import compute_class_weight
# Автоматический расчёт весов
class_weights = compute_class_weight(
'balanced',
classes=np.unique(y_train),
y=y_train
)
print(f"Веса классов: {class_weights}")
# Обычно: [0.52, 10.0] — меньшинство получает больший вес
# Логистическая регрессия с весами
model_lr = LogisticRegression(
class_weight='balanced', # или dict
random_state=42
)
model_lr.fit(X_train, y_train)
# Random Forest с весами
model_rf = RandomForestClassifier(
class_weight='balanced',
n_estimators=100,
random_state=42
)
model_rf.fit(X_train, y_train)
# Или явно указать веса
class_weight_dict = {0: 0.52, 1: 10.0}
model = LogisticRegression(
class_weight=class_weight_dict,
random_state=42
)
5. Threshold Tuning (настройка порога)
from sklearn.metrics import precision_recall_curve, roc_curve
# По умолчанию порог = 0.5
y_pred_default = (y_pred_proba >= 0.5).astype(int)
# Можем изменить порог на основе PR кривой
precision, recall, thresholds = precision_recall_curve(y_test, y_pred_proba)
f1_scores = 2 * (precision * recall) / (precision + recall + 1e-10)
best_threshold_idx = np.argmax(f1_scores)
best_threshold = thresholds[best_threshold_idx]
print(f"Оптимальный порог: {best_threshold:.3f}")
y_pred_tuned = (y_pred_proba >= best_threshold).astype(int)
print(f"F1 Score (оптимальный порог): {f1_score(y_test, y_pred_tuned):.4f}")
print(f"F1 Score (порог 0.5): {f1_score(y_test, y_pred_default):.4f}")
Сравнение методов
from imblearn.over_sampling import SMOTE, ADASYN
from imblearn.under_sampling import RandomUnderSampler
methods = {
'Исходные данные': (X_train, y_train),
'SMOTE': SMOTE().fit_resample(X_train, y_train),
'Random Over': RandomOverSampler().fit_resample(X_train, y_train),
'Random Under': RandomUnderSampler().fit_resample(X_train, y_train),
'class_weight': None # Используется в модели
}
for method_name, (X_method, y_method) in list(methods.items())[:-1]:
model = LogisticRegression(random_state=42)
model.fit(X_method, y_method)
f1 = f1_score(y_test, model.predict(X_test))
roc_auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1])
print(f"{method_name:20} — F1: {f1:.4f}, ROC-AUC: {roc_auc:.4f}")
# class_weight
model = LogisticRegression(class_weight='balanced', random_state=42)
model.fit(X_train, y_train)
f1 = f1_score(y_test, model.predict(X_test))
roc_auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1])
print(f"{'class_weight':20} — F1: {f1:.4f}, ROC-AUC: {roc_auc:.4f}")
Итоговые рекомендации
Для лёгкого дисбаланса (90:10):
- Используй
class_weight='balanced'— быстро и просто - Смени метрику на F1, ROC-AUC, Balanced Accuracy
Для сильного дисбаланса (99:1):
- SMOTE или комбинация SMOTE + undersampling
- Настройка порога классификации
- Ensemble методы (случайный лес работает лучше)
Для очень сильного дисбаланса (999:1):
- Комбинируй все методы: SMOTE + undersampling + class_weight + threshold tuning
- Рассмотри anomaly detection вместо классификации
- Используй One-Class SVM или Isolation Forest
НЕ делай:
- Не используй обычный Accuracy как метрику качества
- Не игнорируй дисбаланс "надеясь, что модель сама разберётся"
- Не переусложняй — начни с простых методов