Введение
Кажется, что нейросети могут все. Мы видим результат их работы в системах распознавания лиц, видеоаналитике. Нас поражает умение нейросетей рисовать изображения, распознавать и синтезировать голос, а большие языковые модели уже давно проходят тест Тьюринга и ведут беседу как живые люди. Но, как показал мой эксперимент, в задачах кредитного скоринга они нисколько не лучше классических 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/