Как работать с несбалансированным набором данных в двоичной классификации - Часть 2

Реализация различных точек разделения с помощью Python

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

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

  • Альтернативные отсечки
  • Взвешивание экземпляров с разными значениями
  • Асимметричная функция потерь

Давайте рассмотрим первый (следующие скоро появятся в частях 3 и 4!). Для этого я буду использовать в качестве примера тот же набор данных, который использовался в Части 1 (с некоторыми изменениями), дисбаланс которого можно увидеть на следующем рисунке:

В моей предыдущей статье о логистической регрессии и ROC (вы можете прочитать ее здесь) я показал вам, как изменение порогового значения вашего классификатора на самом деле влияет на пару истинно положительная частота - ложноположительная частота.

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

Кроме того, мы можем использовать этот метод для устранения дисбаланса данных. А именно, рассмотрим следующие примеры: у нас есть 1000 экземпляров, из которых 900 относятся к классу 1 (предположим, что это «положительный» класс) и только 100 - к классу 0. Наш алгоритм (при допущении логистической регрессии) может легко классифицировать все из эти экземпляры относятся к классу 1, но имеют точность 90%. В деталях матрица путаницы будет выглядеть следующим образом:

Как видите, у нас FNR и TNR равны 0%, а FPR - 10%. Если это так, мы могли бы решить изменить отсечку так, чтобы FPR была минимальной. Конечно, поступая так, мы заплатим цену в виде снижения TPR. Однако мы знаем, что это справедливая цена, чтобы иметь более общий алгоритм, который не предвзято относится к классу большинства, и он не пропустит редкие, но важные отрицательные примеры.

Давайте посмотрим на реализацию на Python, используя следующий сгенерированный набор данных:

from sklearn.datasets import make_classification
import pandas as pd
X, y_tmp = make_classification(
    n_classes=2, class_sep=1, weights=[0.98, 0.02],
    n_informative=10, n_redundant=0,n_repeated=0, 
    n_features=10,  
    n_samples=10000, random_state=123
)
#little trick for terminlogy purpose (positive class = 1, negative class = 0)
y = y_tmp.copy()
for i in range(len(y_tmp)):
  if y_tmp[i]==0:
    y[i] = 1
  else:
    y[i] = 0
df = pd.DataFrame(X)
df['target'] = y
df.target.value_counts().plot(kind='bar', title='Count (target)', color = ['b', 'g'])

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

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
       X, y, test_size=0.33, random_state=42)
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix
model = LogisticRegression(random_state=0).fit(X_train,y_train)

Посмотрим на прогноз на тестовой выборке.

Примечание: функция прогноз () использует в качестве точки отсечения значение по умолчанию 0,5. Чтобы изменить это ограничение, мы определим настраиваемую функцию прогнозирования, которая будет работать непосредственно с прогнозируемыми вероятностями (извлекаемая с помощью функции pred_proba ()).

import matplotlib.pyplot as plt
cm = confusion_matrix(y_test, model.predict(X_test))
fig, ax = plt.subplots(figsize=(8, 8))
ax.imshow(cm)
ax.grid(False)
ax.xaxis.set(ticks=(0, 1), ticklabels=('Predicted 0s', 'Predicted 1s'))
ax.yaxis.set(ticks=(0, 1), ticklabels=('Actual 0s', 'Actual 1s'))
ax.set_ylim(1.5, -0.5)
for i in range(2):
    for j in range(2):
        ax.text(j, i, cm[i, j], ha='center', va='center', color='red')
plt.show()

Как видите, из 84 негативов (класс 0) алгоритм смог классифицировать как таковые только 4. Остальные 80 были классифицированы как положительные. Однако общая точность все еще составляет 98%! Проблема в том, что этот тестовый набор так же несбалансирован, как и исходный, но мы не знаем, будут ли будущие данные такими же. Итак, нам нужен алгоритм, способный хорошо обобщить результат даже на новых, невиданных ранее данных.

Для этого попробуем изменить точку отсечки. Мы знаем, что каждая вероятность выше 0,5 превращается в положительную (класс 1). Поэтому мы хотим усложнить это действие, увеличив точку отсечки. Чтобы лучше понять, давайте взглянем на ROC (на этот раз для всего набора данных):

import plotly.express as px
from sklearn.metrics import roc_curve, auc
model.fit(X, y)
y_score = model.predict_proba(X)[:, 1]
fpr, tpr, thresholds = roc_curve(y, y_score)
fig = px.scatter(
    x=fpr, y=tpr, color = thresholds,
    title=f'ROC Curve (AUC={auc(fpr, tpr):.4f})',
    labels=dict(x='False Positive Rate', y='True Positive Rate', color = 'Threshold'))
fig.add_shape(
    type='line', line=dict(dash='dash'),
    x0=0, x1=1, y0=0, y1=1
)
fig.update_yaxes(scaleanchor="x", scaleratio=1)
fig.update_xaxes(constrain='domain')
fig.show()

Как видите, чем выше точка отсечки, тем ниже TPR и FPR. А именно, давайте попробуем с точкой отсечения 0,9:

import numpy as np
thres = 0.9
preds = model.predict_proba(X_test)
y_pred = np.where(preds[:,1]>thres,1,0)
cm = confusion_matrix(y_test, y_pred)
fig, ax = plt.subplots(figsize=(8, 8))
ax.imshow(cm)
ax.grid(False)
ax.xaxis.set(ticks=(0, 1), ticklabels=('Predicted 0s', 'Predicted 1s'))
ax.yaxis.set(ticks=(0, 1), ticklabels=('Actual 0s', 'Actual 1s'))
ax.set_ylim(1.5, -0.5)
for i in range(2):
    for j in range(2):
        ax.text(j, i, cm[i, j], ha='center', va='center', color='red')
plt.show()

Как видите, теперь алгоритм умеет правильно классифицировать 24 негатива.

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

Выводы

Итак, в этой статье мы увидели, как работать с несбалансированными данными, не изменяя входной набор данных, но вмешиваясь непосредственно в классификатор. В третьей части этой серии статей мы увидим еще один метод, так что следите за обновлениями!

использованная литература