- BrainTools - https://www.braintools.ru -

Борьба с дисбалансом классов. Undersampling

Борьба с дисбалансом классов. Undersampling - 1

На связи KozhinDev и ml-разработчик Приходько Александр. Это вторая статья в цикле публикаций по теме борьбы с дисбалансом классов в машинном обучении [1]. В предыдущей статье [2] мы рассмотрели актуальность данной проблемы и сравнили методы борьбы без внесения изменений в данные: балансировка весов классов и изменение порога принятия решения моделью. В данной части будем тестировать балансировку данных методом undersampling из библиотеки imblearn [3].

Суть данного метода, как нетрудно догадаться, заключается в том, что мы удаляем часть данных в наибольшем классе до тех пор пока количество данных в классах не сравняется. Конечно, данный метод приводит к потери части информации, но при сильном дисбалансе выгода от этого метода может превысить вред. Существует несколько методов определения какие данные будут удалены и именно сравнением подобных методов мы займемся в данной статье.

Ниже код для нашего эксперимента:

Скрытый текст
import numpy as np
import pandas as pd
from dataclasses import dataclass

from sklearn.cluster import MiniBatchKMeans
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.base import clone
from sklearn.metrics import f1_score

from sklearn.linear_model import LogisticRegression
from catboost import CatBoostClassifier
from lightgbm import LGBMClassifier
from xgboost import XGBClassifier

import matplotlib.pyplot as plt
import seaborn as sns
from tqdm import tqdm

from imblearn.under_sampling import (RandomUnderSampler,
                                     ClusterCentroids,
                                     EditedNearestNeighbours,
                                     RepeatedEditedNearestNeighbours,
                                     AllKNN,
                                     InstanceHardnessThreshold,
                                     NearMiss,
                                     NeighbourhoodCleaningRule,
                                     OneSidedSelection,
                                     TomekLinks,
                                     CondensedNearestNeighbour)

import warnings
warnings.filterwarnings("ignore")

@dataclass
class EvalConfig:
   X_train: np.ndarray
   X_test: np.ndarray
   y_train: np.ndarray
   y_test: np.ndarray
   ratios: list[int]
   n_runs: int


def generate_data(n_samples: int, n_features: int, 
                  class_sep: float = 0.75, 
                  random_state: int = 42
                  ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """
    Генерирует синтетический набор данных для бинарной классификации
    и возвращает предварительно разделенные train/test части.

    Параметры:
    - n_samples: общее число образцов
    - n_features: число признаков
    - class_sep: параметр, отвечающий за разделимость классов
    - random_state: воспроизводимость

    Возвращает:
    - X_train, X_test, y_train, y_test (numpy arrays)
    """
    # синтезируем данные
    X, y = make_classification(
        n_samples=n_samples,
        n_features=n_features,
        n_informative=int(n_features / 2),
        n_redundant=0,
        flip_y = 0,
        n_clusters_per_class=2,
        class_sep=class_sep,
        random_state=random_state,
    )

    # делим на train/test
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.25, stratify=y, random_state=random_state
    )
    return X_train, X_test, y_train, y_test

def create_imbalanced_data(X, y, ratio: float, random_state: int = 42):
    """
    Создает дисбаланс в данных, удаляя случайную часть записей

    Параметры:
    - X: тренировочные данные
    - y: тестовые данные
    - ratio: степень дисбаланса
    - random_state: воспроизводимость

    Возвращает:
    - X, y с соотношением majority:minority = ratio:1.
    """
    rng = np.random.default_rng(random_state)

    y_arr = np.asarray(y)
    unique, counts = np.unique(y_arr, return_counts=True)

    # выбираем majority и minority — сортируем индексы по убыванию counts
    order = np.argsort(-counts)  # индексы отсортированы по убыванию (max -> min)
    majority_class = unique[order[0]]
    minority_class = unique[order[1]]

    majority_idx = np.where(y_arr == majority_class)[0]
    minority_idx = np.where(y_arr == minority_class)[0]

    # целевое число элементов минорного класса (не больше существующих)
    n_major = len(majority_idx)
    n_min_target = max(1, int(n_major / ratio))
    n_min_target = min(n_min_target, len(minority_idx))
    chosen_min = rng.choice(minority_idx, size=n_min_target, replace=False)
    chosen_all = np.concatenate([majority_idx, chosen_min])
    rng.shuffle(chosen_all)

    # формируем результат
    if isinstance(X, pd.DataFrame):
        X_sel = X.iloc[chosen_all].reset_index(drop=True)
        y_sel = pd.Series(y_arr[chosen_all]).reset_index(drop=True).astype(int)
    else:
        X_sel = X[chosen_all]
        y_sel = y_arr[chosen_all].astype(int)

    return X_sel, y_sel


def plot_graph(data: pd.DataFrame, balance_method: str) -> None:
    """
    Построение боксплота распределения метрик (например, F1-Score) по уровням дисбаланса.

    Параметры:
    - data: DataFrame с колонками ['Category', 'Values', 'Model', 'Method', 'Metric']
    - balance_method: название метода балансировки (для подписи графика)
    """
    sns.set_theme(style="darkgrid")
    plt.figure(figsize=(14, 7))
    ax = sns.boxplot(
        x='Category',
        y='Values',
        hue='Model',
        data=data,
        palette='coolwarm'
    )
    ax.set_ylim(0, 1)
    plt.xlabel('Уровень дисбаланса')
    plt.ylabel('Распределение F1-Score')
    plt.title(f'Распределение результатов для {balance_method}')
    sns.despine(left=True, top=True)
    plt.legend(title="Model")
    plt.show()


def evaluate_models(balance_method: str, 
                    method_mapping: dict,
                    models: dict[str, object], 
                    cfg: EvalConfig,) -> None:
    """
    Обучает клонированную модель на тренировочных данных и возвращает F1-score
    на тестовой выборке вместе с используемым порогом классификации.
    
    Параметры:
    - balance_method: строка, указывающая метод балансировки.
    - method_mapping: словарь с методами балансировки.
    - models: список моделей.
    - cfg: экземпляр EvalConfig (в текущей реализации параметр здесь не используется
      напрямую, но оставлен для совместимости и возможных расширений).
    """
    results = []
    # извлекаем данные
    X_train, X_test, y_train, y_test = cfg.X_train, cfg.X_test, cfg.y_train, cfg.y_test

    # итерируемся по списку соотношений классов
    for ratio in cfg.ratios:
        desc=f"Соотношение {ratio}:1"

        # для каждого обучения модели создаем свой уникальный дисбаланс, одинаковый для разных экспериментов
        for run in tqdm(range(cfg.n_runs), desc=desc):
            # подготовка обучающей выборки — либо исходная, либо с искусственным дисбалансом
            if ratio == 1:
                X_tr, y_tr = (pd.DataFrame(X_train) if isinstance(X_train, pd.DataFrame) else X_train), y_train
            else:
                X_tr, y_tr = create_imbalanced_data(X_train, y_train, ratio, random_state=run)

            # применение метода балансировки (если не 'naive')
            if (balance_method == 'naive') or (ratio == 1):
                X_res, y_res = X_tr, y_tr

            else:
                # применяем модификацию данных
                sampler = method_mapping[balance_method]
                X_res, y_res = sampler.fit_resample(X_tr, y_tr)

            # перебираем все модели из нашего списка
            for model_name, model in models.items():
                cloned = clone(model)
                cloned.fit(X_res, y_res)
                y_pred = cloned.predict(X_test)

                # собираем результаты
                results.append({"Category": f"{ratio}:1",
                                "Values": f1_score(y_test, y_pred),
                                "Model": model_name,
                                "Method": balance_method,
                                "Run": run,
                                "Ratio": ratio})

    # строим график результатов
    plot_graph(pd.DataFrame(results), balance_method)

Запускаем цикл перебирать наши методы:

for balance_method in method_mapping:
   evaluate_models(balance_method, method_mapping, models, cfg)

Как мы упоминали в первой статье [2], из-за особенностей эксперимента вычисления на GPU только замедлят работу. Мы будем использовать процессор

Тестовый запуск

Для начала проведем оценку деградации F1-score без исправления дисбаланса

Борьба с дисбалансом классов. Undersampling - 2

Результат такой же как был и в прошлой статье – падение метрики при дисбалансе 5 к 1 для моделей градиентного бустинга над решающими деревьями и при 2 к 1 для логистической регрессии.

Random Undersampler

Самый простой метод андерсемплинга – RandomUnderSampler. RandomUnderSampler берёт меньшинство за основу и случайным образом удаляет часть примеров из большинства, чтобы сделать классы сбалансированными.

Борьба с дисбалансом классов. Undersampling - 3

Данный метод показывает очень хороший результат, значимое снижение точности происходит только при соотношении 500 к 1. Скорее всего, это связано с однородностью данных.

Cluster Centroids

При данном методе представители класса большинства объединяются в кластеры, после чего они заменяются на одно значение, которое лежит в центре этого кластера. В результате происходит неполное удаление части данных, сохраняется некое представления об удаленных данных. 

Борьба с дисбалансом классов. Undersampling - 4

Результат практически такой же как и при RandomUnderSampler, единственное при соотношении 1000 к 1 разброс незначительно меньше.

NearMiss

Данный метод основан на определении расстояния между частыми и редкими данными. Этот метод поддерживает 3 подхода:

NearMiss-1

  1. Для каждого примера частого класса вычисляют среднее расстояние до k ближайших примеров меньшинства.

  2. Оставляют те примеры частого класса, у которых это среднее расстояние наименьшее (то есть они ближе к меньшинству).

Полезно, если хотите сохранить примеры, которые находятся рядом с границей.

NearMiss-2

  1. Для каждого примера частого класса вычисляют среднее расстояние до k самых удалённых примеров меньшинства.

  2. Выбирают образцы класса большинства с наименьшим значением такого среднего расстояния (то есть те, которые находятся близко к примерам меньшинства).

Часто дает более «компактный» выбор примеров частого класса.

NearMiss-3

Для каждого примера меньшинства выбирают k ближайших примеров большинства; объединяют эти ближайшие и берут их как подмножество частого класса. Гарантирует покрытие меньшинства — для каждого примера меньшинства берутся ближайшие соседи из большинства.

Мы будем использовать второй вариант как более строгий.

Борьба с дисбалансом классов. Undersampling - 5

Здесь мы видим хороший результат с более плавным снижением качества моделей, несмотря на значительное усиления дисбаланса. 

Группа методов очистки

В эту группу методов входят Edited Nearest Neighbours, Repeated Edited Nearest Neighbours, All KNN и Tomek Links. Их суть заключается в попытке удалить шумные и пересекающиеся данные в категориях, и как следствие этого снизить количество записей. 

Edited Nearest Neighbours (ENN). Для каждого представителя наибольшего класса определяются N ближайших соседей (n_neighbors, по умолчанию 3). Если все ближайшие соседи относятся к наибольшему классу, то он удаляется. Данный метод поддерживает и другой алгоритм – пример будет удаляться если большинство его соседей относятся к частому классу. 

Представим точку класса 0 (частого) и её 3 ближайших соседа, которые имеют следующие классов 0, 0 , 1 и сравним действия алгоритмов:

  • all: не все соседи класса 0 -> запись не удаляется;

  • mode: наиболее частый класс из соседей 0 -> запись удаляется.

По умолчанию используется первый вариант, мы будем использовать второй вариант как более радикальный метод.

Борьба с дисбалансом классов. Undersampling - 6

Repeated Edited Nearest Neighbours (RENN). Отличается от метода ENN многократным повторением [4], пока в очередном проходе ничего не удаляется, либо до заранее заданного числа итераций.

Борьба с дисбалансом классов. Undersampling - 7

All KNN. Если в ENN используется фиксированное число соседей (например, k=3), то в AllKNN выполняется серия шагов, где k последовательно увеличивается от 1 до заданного максимального значения.

Борьба с дисбалансом классов. Undersampling - 8

Tomek Links. Суть данного метода заключается в следующем: если два объекта из разных классов находятся близко, то они находятся в «зоне неопределенности». Данный метод находит такие пары и удаляет представителя наибольшего класса. Удаляться могут в том числе и представители редкого класса, в таком случае данный метод используется для очистки от похожих записей.

Такой подход позволяет избавиться от наиболее похожих представителей частого класса и облегчить обучение. Но в этом же заключается и минус такого подхода –  сознательно избавляясь от наиболее сложноклассифицируемых данных, мы обучаем модель на более простых примерах, что искажает реальную картину.

Tomek Links поддерживает несколько паттернов:

  • auto – удаляет объекты всех классов кроме наименьшего (по умолчанию);

  • majority – удаляет объекты наибольшего класса (для многоклассовой классификации)

  • all – удаляет все найденные пары (для грубой очистки)

Борьба с дисбалансом классов. Undersampling - 9

Ни один из методов этой группы не дал значительного эффекта. Скорее всего это связано с тем, что данные методы, в большей степени, направлены на очистку от шумных данных. У нас же уровень шума не настолько высокий, чтобы эти методы дали значительный эффект.

Группа методов, определяющих сложность классификации данных

К этой группе относятся Condensed Nearest Neighbour, Neighbourhood Cleaning Rule, One Sided Selection и Instance Hardness Threshold. Работа данных методов основывается на различиях в сложности классификации данных простым классификатором. Данные делятся на легко классифицируемые и сложно классифицируемые. 

Condensed Nearest Neighbour. Этот метод заменяет данные на сжатое (condensed) подмножество. 

Сжатое подмножество — это небольшая часть выборки, состоящая из наиболее информативных примеров. Алгоритм CNN поддерживает несколько стратегий определения такого подмножества (sampling_strategy). Так при стратегии “majority” все представители редкого класса будут перенесены в сжатое подмножество. Остальные примеры идут в несжатое подмножество. Затем поочерёдно берут каждый пример из несжатого и подают вместе со сжатым подмножеством на классификатор 1-NN (классификатор ближайшего соседа). Если пример правильно классифицируется — его пропускают, как не несущий никакой дополнительной информации, если классификатор ошибается — переносят в сжатое. Процесс продолжается до тех пор, пока данные не перестанут добавляться в сжатое множество. В результате остаются только информативные примеры, устранение дисбаланса это только побочный и не обязательный эффект.

Данный метод поддерживает несколько механизмов разделения на подмножества:

  • ‘majority’ – уменьшает только мажоритарный класс (самый многочисленный).

  • ‘not minority’ – уменьшает все классы, кроме самого маленького (при наличии более 2-х классов данных).

  • ‘all’ – уменьшает все классы (в сжатое подмножество добавляются по одному представителю каждого класса, выбранные случайным образом).

  • ‘auto’ – эквивалент ‘not minority’.

  • конкретное количество (или долю) экземпляров для каждого класса в виде словаря.

  • список конкретных классов, которые нужно уменьшить.

В нашем эксперименте расчеты при таком подходе займут очень много времени, поэтому проводить мы его не будем

Neighbourhood Cleaning Rule. Представляет собой попытку объединить метод ENN и CNN: сначала проводится удаление части данных с помощью ENN, оставшиеся данные очищаются с помощью CNN.

Борьба с дисбалансом классов. Undersampling - 10

Почему NCR работает быстрее CNN? NCR выполняет простые локальные очистки и сильно сокращает набор перед тем, как запускать более тяжелые операции, тогда как классический CNN — это итеративный, часто квадратичный по числу расстояний процесс, который многократно сравнивает пары объектов.

One Sided Selection. OSS комбинирует две идеи:

  1. Удаление легко классифицируемых примеров класса большинства. Если объект частого класса находится в своей группе (т.е. его ближайшие соседи также принадлежат частому классу), то он не несет новой информации и может быть удален.

  2. Сохранение граничных и сложных объектов. Объекты класса большинства, которые находятся рядом с классом меньшинства, сохраняются — они помогают алгоритму лучше провести границу между классами.

Алгоритм использует Condensed Nearest Neighbor (CNN) для удаления ненужных образцов, а затем применяет Tomek links для очистки границ (удаляет пары «перепутанных» соседей разных классов, когда они мешают корректной классификации).

Борьба с дисбалансом классов. Undersampling - 11

Instance Hardness Threshold. Он оценивает уверенность классификатора в каждом объекте через предсказанную вероятность его истинного класса, полученную с помощью кросс-валидации. Затем он удаляет из целевых классов те объекты, у которых самая низкая вероятность принадлежности к своему классу (т.е. самые «трудные» для классификатора), чтобы получить нужный баланс.

Борьба с дисбалансом классов. Undersampling - 12

Очень интересный результат показала модель IHT: при небольшом дисбалансе она продемонстрировала увеличение точности. Общие результаты также оказались достаточно высокими при соотношении меньше 100 к 1. Другие методы, определяющие сложность классификации данных не дали значительного эффекта.

Итоги

Методы, рассмотренные в данной статье, в отличии от предыдущей, показывают больший разброс скорости работы. Построим график, чтобы сравнить их. Абсолютные значения будут зависеть от вычислительной мощности, поэтому более важно соотношение временных затрат чем конкретные числа.

Борьба с дисбалансом классов. Undersampling - 13

Наш эксперимент показал, что для наших данных наилучшим методом оказалось простое удаление случайных записей в классе большинства (метод RandomUnderSampler) и Cluster Centroids. На третьем месте у нас метод IHT и NearMiss. Первая модель показала хорошие результаты при низком уровне дисбаланса, вторая наоборот – при высоком. В случае более зашумленных данных методы очистки продемонстрировали бы больший результат.

Модели XGBClassifier, LGBMClassifier и CatBoostClassifier показали практически одинаковые результаты с незначительным преимуществом у CatBoostClassifier. LogisticRegression стабильно хуже остальных моделей

Автор: All_Pri

Источник [5]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/20533

URLs in this post:

[1] обучении: http://www.braintools.ru/article/5125

[2] предыдущей статье: https://habr.com/ru/companies/kozhindev/articles/953158/

[3] imblearn: https://imbalanced-learn.org/stable/references/under_sampling.html

[4] повторением: http://www.braintools.ru/article/4012

[5] Источник: https://habr.com/ru/companies/kozhindev/articles/954402/?utm_campaign=954402&utm_source=habrahabr&utm_medium=rss

www.BrainTools.ru

Rambler's Top100