Как работает параметр class_weight в scikit-learn?

116

У меня много проблем с пониманием того, как работает class_weightпараметр в логистической регрессии scikit-learn.

Ситуация

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

Первая попытка: подготовка данных обучения вручную

Я разделил данные, которые у меня были, на отдельные наборы для обучения и тестирования (примерно 80/20). Затем я произвольно произвел выборку обучающих данных вручную, чтобы получить обучающие данные в пропорциях, отличных от 19: 1; от 2: 1 до 16: 1.

Затем я обучил логистическую регрессию на этих различных подмножествах обучающих данных и построил график отзыва (= TP / (TP + FN)) в зависимости от различных пропорций обучения. Конечно, отзыв был рассчитан на непересекающихся образцах TEST, которые имели наблюдаемые пропорции 19: 1. Обратите внимание: хотя я обучал разные модели на разных обучающих данных, я вычислил отзыв для всех из них на одних и тех же (непересекающихся) тестовых данных.

Результаты были такими, как ожидалось: отзыв составил около 60% при тренировочных пропорциях 2: 1 и довольно быстро упал к тому времени, когда он достиг 16: 1. Было несколько соотношений 2: 1 -> 6: 1, где отзыв был прилично выше 5%.

Вторая попытка: поиск по сетке

Затем я хотел протестировать различные параметры регуляризации, поэтому я использовал GridSearchCV и сделал сетку из нескольких значений Cпараметра, а также class_weightпараметра. Чтобы перевести мои пропорции n: m отрицательных: положительных обучающих выборок на язык словарей, class_weightя думал, что просто указываю несколько словарей следующим образом:

{ 0:0.67, 1:0.33 } #expected 2:1
{ 0:0.75, 1:0.25 } #expected 3:1
{ 0:0.8, 1:0.2 }   #expected 4:1

и я также включил Noneи auto.

На этот раз результаты были совершенно потрясающими. Все мои отзывы были крошечными (<0,05) для всех значений, class_weightкроме auto. Так что я могу только предположить, что мое понимание того, как установить class_weightсловарь, неверно. Интересно, что class_weightзначение «авто» при поиске по сетке составляло около 59% для всех значений C, и я предположил, что оно уравновешивается 1: 1?

Мои вопросы

  1. Как правильно использовать данные class_weightдля тренировки, чтобы добиться баланса, отличного от того, что вы им фактически даете? В частности, в какой словарь мне перейти, чтобы class_weightиспользовать n: m пропорций отрицательных: положительных обучающих выборок?

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

  3. Какое autoзначение имеет значение class_weightпропорций? Я читал документацию и полагаю, что «балансирует данные обратно пропорционально их частоте» просто означает, что получается 1: 1. Это верно? Если нет, может кто уточнить?

kilgoretrout
источник
Когда используется class_weight, функция потерь изменяется. Например, вместо кросс-энтропии это взвешенная кросс-энтропия. towardsdatascience.com/...
Prashanth

Ответы:

123

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

Как class_weightработает: он наказывает ошибки в образцах class[i]с помощью class_weight[i]вместо 1. Таким образом, более высокий вес класса означает, что вы хотите уделять больше внимания классу. Из того, что вы говорите, кажется, что класс 0 в 19 раз чаще, чем класс 1. Поэтому вам следует увеличить class_weightкласс 1 по сравнению с классом 0, скажем {0: .1, 1: .9}. Если class_weightсумма не равна 1, это в основном изменит параметр регуляризации.

Чтобы узнать class_weight="auto", как работает, вы можете посмотреть это обсуждение . В версии для разработчиков вы можете использовать class_weight="balanced", что легче понять: это в основном означает репликацию меньшего класса, пока у вас не будет столько же образцов, сколько в более крупном, но неявным образом.

Андреас Мюллер
источник
1
Спасибо! Быстрый вопрос: я упомянул отзыв для ясности, и на самом деле я пытаюсь решить, какой AUC использовать в качестве моей меры. Насколько я понимаю, я должен либо максимизировать площадь под кривой ROC, либо площадь под кривой отзыва и точности, чтобы найти параметры. Выбрав параметры таким образом, я считаю, что выбираю порог для классификации, скользя по кривой. Вы это имели в виду? Если да, то на какую из двух кривых лучше всего смотреть, если моя цель - захватить как можно больше TP? Кроме того, спасибо за вашу работу и вклад в scikit-learn !!!
kilgoretrout
1
Я думаю, что использование ROC было бы более стандартным способом, но я не думаю, что будет большая разница. Однако вам нужен какой-то критерий, чтобы указать точку на кривой.
Андреас Мюллер
3
@MiNdFrEaK Я думаю, что Эндрю имеет в виду, что оценщик копирует образцы в классе меньшинства, так что выборка разных классов сбалансирована. Это просто неявная передискретизация.
Шон ТИАН
8
@MiNdFrEaK и Шон Тиан: Классификаторы на основе SV не производят больше выборок меньших классов, когда вы используете «сбалансированный». Он буквально наказывает ошибки, допущенные в младших классах. Сказать иначе - ошибка и вводит в заблуждение, особенно в случае больших наборов данных, когда вы не можете позволить себе создавать больше выборок. Этот ответ необходимо отредактировать.
Пабло Ривас
4
scikit-learn.org/dev/glossary.html#term-class-weight Веса классов будут использоваться по-разному в зависимости от алгоритма: для линейных моделей (таких как линейная SVM или логистическая регрессия) веса классов изменят функцию потерь на взвешивание потерь каждого образца по его весу класса. Для древовидных алгоритмов веса классов будут использоваться для повторного взвешивания критерия разделения. Обратите внимание, однако, что эта перебалансировка не принимает во внимание вес образцов в каждом классе.
Prashanth
2

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

РЕЗЮМЕ

  • для умеренно несбалансированных данных БЕЗ шума нет большой разницы в применении весов классов
  • для умеренно несбалансированных данных С шумом и сильно несбалансированных, лучше применять веса классов
  • param class_weight="balanced"работает достойно, если вы не хотите оптимизировать вручную
  • с class_weight="balanced"вами захватить больше истинные события (выше ИСТИНА вспоминания) , но и вы, скорее всего , чтобы получить ложные сигналы (понизить ИСТИНУ точности)
    • в результате общий% ИСТИНА может быть выше фактического из-за всех ложных срабатываний.
    • AUC может ввести вас в заблуждение, если ложные срабатывания являются проблемой
  • нет необходимости изменять порог принятия решения на% дисбаланса, даже при сильном дисбалансе, можно оставить 0,5 (или где-то около этого, в зависимости от того, что вам нужно)

NB

Результат может отличаться при использовании RF или GBM. sklearn не поддерживает class_weight="balanced" GBM, но lightgbm имеетLGBMClassifier(is_unbalance=False)

КОД

# scikit-learn==0.21.3
from sklearn import datasets
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score, classification_report
import numpy as np
import pandas as pd

# case: moderate imbalance
X, y = datasets.make_classification(n_samples=50*15, n_features=5, n_informative=2, n_redundant=0, random_state=1, weights=[0.8]) #,flip_y=0.1,class_sep=0.5)
np.mean(y) # 0.2

LogisticRegression(C=1e9).fit(X,y).predict(X).mean() # 0.184
(LogisticRegression(C=1e9).fit(X,y).predict_proba(X)[:,1]>0.5).mean() # 0.184 => same as first
LogisticRegression(C=1e9,class_weight={0:0.5,1:0.5}).fit(X,y).predict(X).mean() # 0.184 => same as first
LogisticRegression(C=1e9,class_weight={0:2,1:8}).fit(X,y).predict(X).mean() # 0.296 => seems to make things worse?
LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X).mean() # 0.292 => seems to make things worse?

roc_auc_score(y,LogisticRegression(C=1e9).fit(X,y).predict(X)) # 0.83
roc_auc_score(y,LogisticRegression(C=1e9,class_weight={0:2,1:8}).fit(X,y).predict(X)) # 0.86 => about the same
roc_auc_score(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X)) # 0.86 => about the same

# case: strong imbalance
X, y = datasets.make_classification(n_samples=50*15, n_features=5, n_informative=2, n_redundant=0, random_state=1, weights=[0.95])
np.mean(y) # 0.06

LogisticRegression(C=1e9).fit(X,y).predict(X).mean() # 0.02
(LogisticRegression(C=1e9).fit(X,y).predict_proba(X)[:,1]>0.5).mean() # 0.02 => same as first
LogisticRegression(C=1e9,class_weight={0:0.5,1:0.5}).fit(X,y).predict(X).mean() # 0.02 => same as first
LogisticRegression(C=1e9,class_weight={0:1,1:20}).fit(X,y).predict(X).mean() # 0.25 => huh??
LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X).mean() # 0.22 => huh??
(LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict_proba(X)[:,1]>0.5).mean() # same as last

roc_auc_score(y,LogisticRegression(C=1e9).fit(X,y).predict(X)) # 0.64
roc_auc_score(y,LogisticRegression(C=1e9,class_weight={0:1,1:20}).fit(X,y).predict(X)) # 0.84 => much better
roc_auc_score(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X)) # 0.85 => similar to manual
roc_auc_score(y,(LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict_proba(X)[:,1]>0.5).astype(int)) # same as last

print(classification_report(y,LogisticRegression(C=1e9).fit(X,y).predict(X)))
pd.crosstab(y,LogisticRegression(C=1e9).fit(X,y).predict(X),margins=True)
pd.crosstab(y,LogisticRegression(C=1e9).fit(X,y).predict(X),margins=True,normalize='index') # few prediced TRUE with only 28% TRUE recall and 86% TRUE precision so 6%*28%~=2%

print(classification_report(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X)))
pd.crosstab(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X),margins=True)
pd.crosstab(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X),margins=True,normalize='index') # 88% TRUE recall but also lot of false positives with only 23% TRUE precision, making total predicted % TRUE > actual % TRUE
citynorman
источник