Credit Scoring: Одинокое дерево, целый лес и разочарование в нейросетях

от автора

Введение

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

Ход эксперимента

В качестве датасета были взяты данные кредитного скоринга с https://www.kaggle.com/competitions/GiveMeSomeCredit. Сначала мы обучили решающее дерево, с кросс-валидацией.  График зависимости roc-auc от количества элементов выборки (Ореол вокруг кривой – это дисперсия (разброс) при кроссвалидации):

А вот таблица результирующих метрик:

Метрика

CV (5-fold)

Тест

accuracy

0.794 ± 0.017

0.798

precision

0.209 ± 0.011

0.214

recall

0.736 ± 0.023

0.755

f1

0.326 ± 0.012

0.334

roc-auc

0.829 ± 0.005

0.840

Интерпретация результатов:

  • ROC-AUC ≈ 0.84 — модель неплохо ранжирует заёмщиков по риску; это главная метрика для скоринга.

  • Recall ≈ 75% — находит 3 из 4 реальных просрочек, но с большим числом ложных тревог.

  • Precision ≈ 21% — из 10 предсказанных просрочек только ~2 реальные.

  • Accuracy ≈ 80% — на фоне 93% «хороших» клиентов выглядит приемлемо, но сама по себе обманчива.

  • F1 ≈ 33% – очень мало (модель бесполезна): много пропусков либо много ложных тревог, либо и то и другое. В нашем случае ужасающее количество ложных тревог: 8 из 10 заемщиков, которым мы отказываем, на самом деле хорошие заемщики и вовремя бы вернули кредит.

Стоит заметить, что такие результаты мы получили при включенной опции  весов «balanced» – специальная опция решающего дерева, предназначенная для обучения на несбалансированных данных:

def build_decision_tree_model() -> PreprocessingClassifier:    return PreprocessingClassifier(        preprocessor=MissingValueHandler(),        model=DecisionTreeClassifier(            random_state=RANDOM_STATE,            class_weight="balanced",            max_depth=8,        ),    )

Если убрать эту опцию, то тогда дерево просто всегда будет «говорить», что не будет просрочки. И тогда точность достигнет 93% – ровно столько, сколько процентов нормальных заемщиков. Но в этом случае будет еще ужаснее полнота и другие метрики:

Метрика

CV (5-fold)

Тест

accuracy

0.934 ± 0.0006

0.936

precision

0.539 ± 0.012

0.568

recall

0.181 ± 0.009

0.172

f1

0.271 ± 0.011

0.264

roc-auc

0.839 ± 0.0055

0.850

Именно поэтому accuracy в отрыве от остальных показателей – бесполезная метрика. Теперь проиграемся еще с одним параметром: максимальная глубина (max_depth):

Метрика

3

8

15

accuracy

0.677

0.798

0.819

precision

0.155

0.214

0.210

recall

0.853

0.755

0.613

f1

0.262

0.334

0.312

roc-auc

0.821

0.840

0.721

Данная таблица иллюстрирует, что максимальная глубина была выбрана достаточно оптимально – ее увеличение ведет к переобучению, а уменьшение к потере точности. Да, у нас при max_depth=3 возрос recall, но упала точность.

В общем, одиночное решающее дерево никуда не годится. Нужен целый «лес» – random forest.

Сравним его метрики с простым решающим деревом:

Метрика

Decision Tree

Random Forest

accuracy

0.798

0.800

precision

0.214

0.219

recall

0.755

0.770

f1

0.334

0.341

roc-auc

0.840

0.864

Как видно из таблицы, метрики улучшились (все), но незначительно.

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

Метрика

До

После

accuracy

0.800

0.812

precision

0.219

0.227

recall

0.770

0.751

f1

0.341

0.349

roc-auc

0.864

0.866

Что мы тут видим? Точность незначительно улучшилась, полнота упала, в целом метрики чуть-чуть улучшились. Вот какие параметры были подобраны алгоритмам (сравним их с параметрами по умолчанию):

Параметр

Подобрано

По умолчанию

Изменение

n_estimators

100

100

без изменений

max_depth

10

8

глубже (+2)

min_samples_leaf

20

1

сильно больше

min_samples_split

50

2

сильно больше

max_features

sqrt

sqrt

без изменений

Сделаем второй прогон, с другим RANDOM_STATE:

Метрика

Старый прогон

Новый прогон

accuracy

0.812

0.815

precision

0.227

0.233

recall

0.751

0.763

f1

0.349

0.357

roc-auc

0.866

0.872

Видим, что метрики незначительно поменялись (стали чуть-чуть получше). Смотрим как изменились настройки:

Параметр

Старый прогон

Новый прогон

Изменение

n_estimators

100

200

увеличился

max_depth

10

10

без изменений

min_samples_leaf

20

20

без изменений

min_samples_split

50

50

без изменений

max_features

sqrt

sqrt

без изменений

Видим, что тут увеличилось n_estimators (общее количество деревьев решений в ансамбле).

И так, мы выяснили, что ни решающее дерево, ни случайный лес тут не работают. Попробуем бустинг. Сравним:

Метрика

Random Forest

Hist gradient boosting

accuracy

0.812

0.801

precision

0.228

0.220

recall

0.750

0.772

f1

0.350

0.343

roc-auc

0.866

0.867

Как видим, используя лес и бустинг, мы смогли лишь слегка увеличить accuracy, при этом precision оставили на прежнем уровне, recall тоже смогли чуть-чуть поднять.

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

Метрика

Hist gradient boosting

Нейросеть

accuracy

0.801

0.815

precision

0.220

0.222

recall

0.772

0.702

f1

0.343

0.338

roc-auc

0.867

0.835

 

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

Попробуем больше слоев, например, не (64, 32), а (128, 64, 32):

Метрика

Старая нейросеть

Новая нейросеть

accuracy

0.815

0.812

precision

0.222

0.220

recall

0.702

0.705

f1

0.338

0.335

roc-auc

0.835

0.835

Практически, ничего не изменилось, мы только усилили переобучение. По сути, мы достигли предела. Дальше остается только работа с данными: в выборке просто не хватает фич.

 Заключение

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

1.     Проклятие несбалансированности. Самая основная причина катастрофически низкого F1 – несбалансированность выборки. В датасете 93% «хороших» и 7% «плохих» элементов. При таких условиях даже идеальная модель будет делать ошибки, так как она вынуждена балансировать между recall (найти всех плохих) и precision (не забраковать ошибочно хороший элемент). Включение опции class_weight=’balanced’  сдвигает модель в сторону поиска «плохих» объектов, но значительно увеличивает количество ложных тревог. И это не потому, что модель «плохая», а потому что такова объективная реальность: «плохие» и «хорошие» заемщики сильно пересекаются в пространстве признаков.

2.     Сила признаков (Причина «Потолка ROC-AUC»). Метрика ROC-AUC показывает, насколько хорошо модель отличает «плохие» объекты от «хороших», то есть ставит первых выше вторых по вероятности. Значение 0.86 – это довольно хороший, но отнюдь не выдающиеся результат. Мы видим, что бустинг и нейросеть не помогают, это значит, что все полезные сигналы уже «выжаты» из фич, и между фичами и выходными признаками нет сложных нелинейных зависимостей, которые могла бы обнаружить. Далее, сами признаки довольно слабые: возраст, количество открытых кредитов, количество иждивенцев – они не позволяют однозначно предсказать, вернет ли заемщик долг.  

Можно ли улучшить результат? Несмотря на недостаток фич, полагаю, что да, возможно. Во-первых, в качестве метрики использовать не AUC-ROC, а AUC-PR, он лучше подходит для несбалансированной выборки. Во-вторых, перед выбором и обучением провести фиче-инженеринг, проанализировать, какие фичи коррелирует с целевыми признаками,  в-третьих, попробовать составные фичи, когда мы вычисляем новые фичи на основании существующих, делая над ними различные математические операции. В-четвертых поработать с заполнением пропущенных данных. Возможно, отсутствие каких-то данных это уже сам по себе «красный» флаг – новая фича.

ссылка на оригинал статьи https://habr.com/ru/articles/1062568/