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

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

В этой статье вы узнаете другой подход. Мы рассмотрим, как использовать кластерный анализ для устранения несбалансированной классификации.

Введение

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

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

Несбалансированные распределения являются проблемой для алгоритмов машинного обучения. Информации о меньшинстве относительно мало. Это препятствует способности алгоритмов обучать хорошие модели, потому что они склонны смещаться в сторону класса большинства.

Как справиться с дисбалансом классов

Существует три стандартных подхода к устранению дисбаланса классов:

  1. Методы повторной выборки;
  2. чувствительные к стоимости модели;
  3. Настройка порога.

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

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

Вот пример работы методов повторной выборки с использованием библиотеки imblearn:

from sklearn.datasets import make_classification
from imblearn.over_sampling import SMOTE

X_train, y_train = make_classification(n_samples=500, n_features=5, n_informative=3)
X_res, y_res = SMOTE().fit_resample(X_train, y_train)

Методы повторной выборки универсальны и легко сочетаются с любым алгоритмом обучения. Но у них есть некоторые ограничения.

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

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

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

Борьба с дисбалансом классов с помощью кластеризации

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

Например, ADASYN — популярный метод избыточной выборки. Он создает искусственные экземпляры, используя случаи из класса меньшинства, чьи ближайшие соседи принадлежат к классу большинства.

Поиск пограничных случаев с помощью кластерного анализа

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

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

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

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

Как построить иерархическую модель для несбалансированной классификации

Мы строим иерархическую модель на основе двух уровней.

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

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

На обоих уровнях проблема несбалансированности уменьшается, что упрощает задачу моделирования.

Реализация Python

Описанный выше метод называется ICLL (для несбалансированной классификации с помощью многоуровневого обучения). Вот его реализация:

from collections import Counter
from typing import List

import numpy as np
import pandas as pd
from scipy.cluster.hierarchy import linkage, fcluster
from scipy.spatial.distance import pdist


class ICLL:
    """
    Imbalanced Classification via Layered Learning
    """

    def __init__(self, model_l1, model_l2):
        """
        :param model_l1: Predictive model for the first layer
        :param model_l2: Predictive model for the second layer
        """
        self.model_l1 = model_l1
        self.model_l2 = model_l2
        self.clusters = []
        self.mixed_arr = np.array([])

    def fit(self, X: pd.DataFrame, y: np.ndarray):
        """
        :param X: Explanatory variables
        :param y: binary target variable
        """
        assert isinstance(X, pd.DataFrame)
        X = X.reset_index(drop=True)

        if isinstance(y, pd.Series):
            y = y.values

        self.clusters = self.clustering(X=X)

        self.mixed_arr = self.cluster_to_layers(clusters=self.clusters, y=y)

        y_l1 = y.copy()
        y_l1[self.mixed_arr] = 1

        X_l2 = X.loc[self.mixed_arr, :]
        y_l2 = y[self.mixed_arr]

        self.model_l1.fit(X, y_l1)
        self.model_l2.fit(X_l2, y_l2)

    def predict(self, X):
        """
        Predicting new instances
        """

        yh_l1, yh_l2 = self.model_l1.predict(X), self.model_l2.predict(X)

        yh_f = np.asarray([x1 * x2 for x1, x2 in zip(yh_l1, yh_l2)])

        return yh_f

    def predict_proba(self, X):
        """
        Probabilistic predictions
        """

        yh_l1_p = self.model_l1.predict_proba(X)
        try:
            yh_l1_p = np.array([x[1] for x in yh_l1_p])
        except IndexError:
            yh_l1_p = yh_l1_p.flatten()

        yh_l2_p = self.model_l2.predict_proba(X)
        yh_l2_p = np.array([x[1] for x in yh_l2_p])

        yh_fp = np.asarray([x1 * x2 for x1, x2 in zip(yh_l1_p, yh_l2_p)])

        return yh_fp

    @classmethod
    def cluster_to_layers(cls, clusters: List[np.ndarray], y: np.ndarray) -> np.ndarray:
        """
        Defining the layers from clusters
        """

        maj_cls, min_cls, both_cls = [], [], []
        for clst in clusters:
            y_clt = y[np.asarray(clst)]

            if len(Counter(y_clt)) == 1:
                if y_clt[0] == 0:
                    maj_cls.append(clst)
                else:
                    min_cls.append(clst)
            else:
                both_cls.append(clst)

        both_cls_ind = np.array(sorted(np.concatenate(both_cls).ravel()))
        both_cls_ind = np.unique(both_cls_ind)

        if len(min_cls) > 0:
            min_cls_ind = np.array(sorted(np.concatenate(min_cls).ravel()))
        else:
            min_cls_ind = np.array([])

        both_cls_ind = np.unique(np.concatenate([both_cls_ind, min_cls_ind])).astype(int)

        return both_cls_ind

    @classmethod
    def clustering(cls, X, method='ward'):
        """
        Hierarchical clustering analysis
        """

        d = pdist(X)

        Z = linkage(d, method)
        Z[:, 2] = np.log(1 + Z[:, 2])
        sZ = np.std(Z[:, 2])
        mZ = np.mean(Z[:, 2])

        clust_labs = fcluster(Z, mZ + sZ, criterion='distance')

        clusters = []
        for lab in np.unique(clust_labs):
            clusters.append(np.where(clust_labs == lab)[0])

        return clusters

Часть кластеризации выполняется автоматически без участия пользователя. Итак, единственное, что вам нужно определить, — это алгоритм обучения на каждом уровне иерархии.

И ниже приведен пример того, как вы можете использовать этот метод. В этом примере модель на каждом уровне представляет собой случайный лес.

import pandas as pd
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier as RFC

# https://github.com/vcerqueira/blog/blob/main/src/icll.py
from src.icll import ICLL

# creating a dummy data set
X, y = make_classification(n_samples=500, n_features=5, n_informative=3)
X = pd.DataFrame(X)

# creating a instance of the model
icll = ICLL(model_l1=RFC(), model_l2=RFC())
# training
icll.fit(X, y)
# probabilistic predictions
probs = icll.predict_proba(X)

Более серьезный пример

Чем иерархический метод отличается от повторной выборки?

Ниже приведено сравнение, основанное на наборе данных, связанных с диабетом. Вы можете проверить ссылку [1] для получения подробной информации. Вот как мы можем применить оба метода к этим данным:

from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_curve, roc_auc_score
from imblearn.over_sampling import SMOTE

# loading diabetes dataset https://github.com/vcerqueira/blog/tree/main/data
data = pd.read_csv('data/pima.csv')

X, y = data.drop('target', axis=1), data['target']
X = X.fillna(X.mean())

# train test split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)

# resampling with SMOTE
X_res, y_res = SMOTE().fit_resample(X_train, y_train)

# creating the models
smote = RFC()
icll = ICLL(model_l1=RFC(), model_l2=RFC())

# training 
smote.fit(X_res, y_res)
icll.fit(X_train, y_train)

# inference
smote_probs = smote.predict_proba(X_test)
icll_probs = icll.predict_proba(X_test)

Ниже приведена кривая ROC для каждого подхода:

Кривая ICLL ближе к верхнему левому краю, что указывает на то, что это лучшая модель.

Гораздо больше экспериментов было проведено в статье в ссылке [2], где была представлена ​​ICLL. Результаты показывают, что ICLL обеспечивает конкурентоспособность в задачах несбалансированной классификации. Проверить код для экспериментов можно на Github.

Ключевые выводы

  • Несбалансированная классификация — важная задача науки о данных;
  • Повторная выборка обучающего набора является распространенным подходом к решению этих проблем. Но это может привести к потере информации или распространению шума. Распространенными альтернативами являются пороговая настройка или модели, чувствительные к стоимости;
  • Вы также можете использовать иерархические методы для решения проблемы дисбаланса;
  • ICLL — это иерархический метод несбалансированной классификации. Ему не нужны никакие пользовательские параметры, кроме алгоритма обучения. ICLL обеспечивает конкурентоспособную производительность с помощью методов передискретизации.

Надеюсь, вы найдете этот метод полезным. Спасибо за прочтение и до встречи в следующей истории!

Рекомендации

[1] Набор данных о диабете индейцев пима (лицензия GPL-3)

[2] Черкейра, В., Торго, Л., Бранко, П., и Беллинджер, К. (2022). Автоматическая несбалансированная классификация с помощью многоуровневого обучения. Машинное обучение, 1–22.

[3] Бранко, Паула, Луис Торго и Рита П. Рибейро. «Обзор прогнозного моделирования в несбалансированных доменах». Вычислительные исследования ACM (CSUR) 49.2 (2016): 1–50.