← Назад к вопросам

Что такое дисбаланс классов и как с ним бороться?

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 как метрику качества
  • Не игнорируй дисбаланс "надеясь, что модель сама разберётся"
  • Не переусложняй — начни с простых методов