Впервые опубликовано в блоге ведущего на https://anchormen.nl/blog/data-science-ai/imbalanced-datasets-machine-learning-cycle/. Спасибо моим коллегам из Anchormen за предоставленное время и ресурсы для написания этой записи в блоге!

Несбалансированные данные

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

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

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

Выборка вверх и вниз в помощь

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

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

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

Очень важно применять SMOTE только к обучающей выборке по той же причине, по которой мы нормализуем данные отдельно в обучающей и тестовой выборке. Если мы генерируем синтетические данные на основе всего набора данных (и особенно точек данных, которые попадают в тестовый набор), мы не можем сказать, что модель оценивалась на невидимых данных. Если наши синтетические данные основаны на некоторых точках данных в тестовом наборе, то модель будет косвенно «видеть» данные в тестовом наборе, потому что синтетические точки данных отражают некоторую информацию, полученную данными тестового набора. Эта ловушка называется утечкой информации, и ее лучше всего избегать, обеспечив раздельное прохождение обучающего и тестового наборов через конвейер предварительной обработки, например с помощью sci-kit Learn Конвейерныйкласс, который позволяет объединять преобразователи и оценщики в цепочку и применять их к обучающему и тестовому наборам по отдельности, не вызывая утечки информации.

Метрики оценки

Итак, мы обучили нашу модель на данных с повышенной дискретизацией, чтобы убедиться, что мы максимально используем наше меньшинство, желаемый класс. Как мы оцениваем модель? «В предыдущем сообщении в блоге мы говорили о важности выбора правильной метрики для решаемой нами задачи прогнозирования. Например, для клиента, занимающегося производством запчастей для автомобилей и грузовиков, мы хотели спрогнозировать качество производимого продукта на основе входных данных, поступающих от производственных машин. Мы явно сосредоточились на хороших продуктах, но точное определение некачественных продуктов и характеристик оборудования, которые могут правильно предсказать их, было первостепенным для производственных процессов нашего клиента. Если некачественные товары (Класс 1 в таблице ниже) составляют около 5% всех товаров и нам удалось правильно определить 90 хороших товаров (Класс 0 в таблице ниже) и 1 некачественный товар из 100 товаров , мы получаем 91% точность* для модели (=(истинно положительные + истинно отрицательные) / все точки данных). Но хотя эта оценка точности звучит великолепно, мы обнаружили только 1 из 5 некачественных продуктов; наша точность для миноритарного класса составляет 1/6 или 16,67%! (= Истинные положительные результаты / (Истинные положительные результаты + Ложные положительные результаты).

Это означает, что точность не является для нас критерием оценки. Точность может быть, но только если мы посмотрим на точность для класса меньшинства. Что мы хотим знать о нашей модели, так это не только то, сколько из 5 (класса меньшинства) плохих продуктов мы правильно классифицировали; мы хотим знать, сколько из всех плохих продуктов мы смогли обнаружить (и сколько мы пропустили). Это наша оценка отзыва, которая указывает на чувствительность модели. В нашем примере мы идентифицировали 1 из 5, что дает нам отзыв 25% (=Истинно положительные/(Истинно положительные + Ложноотрицательные)), и это означает, что нам нужно улучшить нашу модель (или получить более релевантные данные). ). Отзыв предоставляет более сбалансированную метрику для оценки модели: он не только говорит, сколько ваших этикеток верны, но и сколько потенциальных продуктов вы пропустили. Это дает нам представление о том, насколько хорошо модель справляется со всеми классами.

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

Прогнозирование типов клиентов: пример использования несбалансированных данных

Давайте проиллюстрируем преимущества избыточной выборки и найдем правильный баланс между полнотой и точностью с помощью открытого набора данных. Мы используем здесь небольшой набор данных, взятый из Kaggle, который включает информацию о различных клиентах, такую ​​как пол, уровень образования и сумма кредита, с целевой характеристикой хорошо или плохо». клиент. Из небольшого фона, доступного для этого набора данных, неясно, какие показатели используются для классификации клиента как хорошего или плохого, но поскольку это пример набора данных, который мы используем для обсуждения другого вопроса, нам придется отложить этот важный вопрос. Хотя мы обходим эту тему в этом блоге, важно отметить, что решение этой проблемы — это первый шаг, который мы делаем в нашем предварительном сканировании проекта и данных в Anchormen: мы сначала стремимся понять, что такое бизнес-вопросы и ключевые показатели эффективности. , и какую часть информации, касающейся этих бизнес-вопросов, мы можем извлечь из доступных данных. Если нет четкой связи между данными и желаемыми выводами и если данные сомнительны, модель с высокой точностью и полнотой бесполезна для наших клиентов.

import matplotlib.pyplot as plt # plotting
import seaborn as sns # prettier plotting
import numpy as np # linear algebra
import os # accessing directory structure
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
import mlflow
from mlflow.models.signature import infer_signature
from mlflow.types.schema import Schema, ColSpec
from mlflow.types.schema import Schema, ColSpec
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
from hyperopt.pyll import scope
import hyperopt
from sklearn.metrics import recall_score, f1_score, roc_auc_score, plot_roc_curve, classification_report
import category_encoders
from imblearn.pipeline import Pipeline as SMOTEpipe
from imblearn.over_sampling import SMOTE, SMOTENC
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.feature_selection import SelectFromModel, VarianceThreshold
%matplotlib inline
from IPython.display import Image
import warnings
warnings.filterwarnings('ignore')

Проверка данных и проектирование функций

После того, как мы загрузили необходимые библиотеки, мы прочитаем файл и проверим набор данных: 1723 наблюдения и 14 столбцов.

df1 = pd.read_csv('clients.csv', delimiter=',')
df1.dataframeName = 'clients.csv'
print(f'There are {df1.shape[0]} rows and {df1.shape[1]} columns')

1723 строки и 14 столбцов.

df1.head()

df1.describe(include='all')

Мы можем легко определить целевую функцию по столбцу с четким названием bad_client_target. Затем мы выберем числовые и двоичные столбцы и определим их как numeric_features, чтобы отличить их от categorical_features. > и binary_features, так как они будут обрабатываться по-разному на этапе конвейера предварительной обработки обучения модели. Мы не будем использовать столбец месяц, поскольку, хотя месяц зарегистрированной транзакции или данные клиента и запись метки могут соотноситься с меткой клиента, мы ищем предсказуемые данные. особенности класса клиента (не обязательно, когда событие мошенничества, возможно, было зарегистрировано).

Глядя на распределение меток клиентов, мы замечаем огромный перекос в данных: только 11,38% наших наблюдений — плохие метки клиентов. У нас есть несбалансированный набор данных с классом беспокойства, являющимся классом меньшинства. В этом вам поможет SMOTE.

numeric_features = ['credit_amount', 'age', 'income']
binary_features = ['having_children_flg', 'is_client']
categorical_features = ['sex', 'education', 'product_type', 'family_status', 'phone_operator', 'region', 'credit_term']
features = numeric_features + categorical_features + binary_features
target = 'bad_client_target'
datetime_col = ['month']
df1.loc[:, target].value_counts()

0 1527
1 196
Имя: bad_client_target, dtype: int64

Далее мы рассмотрим корреляции между признаками. Мы видим, что credit_amount несколько коррелирует как с доходом(0,37), так и с credit_term (0,49). Это было бы хорошей причиной отказаться от credit_amount, чтобы избежать коллинеарности между функциями, поскольку некоторые модели не очень хорошо оснащены обработкой коллинеарности. В этом блоге мы будем обучать модель Random Forest. Одна из уловок этого в остальном отличного алгоритма заключается в том, что коррелированным функциям могут быть присвоены аналогичные оценки важности и пониженные оценки важности по сравнению с моделью, которая исключает подмножество этих коррелированных функций. Это связано с процедурой алгоритма: на каждой итерации слабая модель обучается на случайном подмножестве функций и подмножестве данных. После того, как одна из коррелированных функций используется в качестве предиктора, последующая коррелированная функция не добавляет многого к уменьшению примеси (т. Е. Насколько вероятно, что мы неправильно классифицируем случайное наблюдение, учитывая разделение дерева с использованием этой функции), как это уже было сделано. по первому коррелированному признаку. Хотя это обычно не влияет на производительность модели, поскольку несколько деревьев с разными наборами функций совместно дают точный прогноз, это проблема на этапе объяснения модели.

Мы начнем с обучения модели всем функциям и рассчитаем их важность, чтобы определить, следует ли нам исключить те, которые имеют очень низкую важность. В качестве примечания, количество функций в этом наборе данных (12) довольно мало, особенно по сравнению с количеством функций, которые нам приходилось обрабатывать в нашем проекте с клиентом, производящим запчасти для автомобилей и грузовиков (более 300). . Если вы столкнулись со вторым случаем, мы рекомендуем выполнить этап выбора функций перед обучением модели. Мы использовали метапреобразователь SelectFromModelиз модуля scikit-learn feature_selection и установите нижний порог значимости 0,05. Порог, который мы установили, был произвольным, чтобы получить управляемое количество функций; но другая, более строгая стратегия может заключаться в выборе только признаков с показателями важности выше среднего показателя важности.

sns.heatmap(df1.corr())
plt.show()

print(f"Correlation between credit_amount and the following features:\n{df1.corr().loc['credit_amount', ['income', 'credit_term']]}")

Корреляция между credit_amount и следующими характеристиками:
доход 0,372995
кредит_термин 0,497040
Имя: кредит_сумма, dtype: float64

Затем мы проверяем графики распределения выбранных столбцов, которые говорят нам, что credit_amount, возраст и доход перекошены вправо, а срок кредита выглядит как категориальный, а не непрерывный признак, при этом большинство значений сосредоточено вокруг нескольких значений, а именно: 12, 6, 10, 18 и 24 и 3 (в таком порядке частоты).

df1[numeric_features + binary_features + [target]].hist(density=True, figsize=(10,10))
plt.show()

Мы переходим к категориальным характеристикам и видим, что пол(мужской, женский), семейный_статус(замужем, не состоит в браке, Другой) и регион(от 0 до 2) имеют 2–3 уровня, с которыми можно легко справиться с помощью любой предварительной обработки категориальных признаков (например, горячего кодирования).

df1[categorical_features].agg(['count', 'size', 'nunique'])

Функции с более чем 5 уровнями сложны по двум основным причинам:

  1. Если их много и они имеют большое значение, например, product_type. Функция product_type оказывается очень важной — см. раздел об обучении модели в этом посте — и имеет 22 уровня (уникальные значения)! Это может привести к тому, что модель предскажет тип клиента по категории продукта. Это не говорит нам, какие основные свойства клиента могут помочь нам предсказать, хороший он или плохой; скорее, он говорит нам, для каких категорий продуктов мы с большей вероятностью столкнемся с плохим клиентом. Это приведет к тому, что мы будем дискриминировать более частые категории продуктов с большой долей плохих клиентов и, возможно, неправильно классифицировать настоящих плохих клиентов в менее частых категориях продуктов. Один из способов избежать этой опасности — либо исключить категориальные признаки с большим количеством уровней, либо уменьшить количество уровней, объединив низкочастотные уровни с новой меткой (например, «другое»).
  2. При использовании горячего кодирования или бинаризатора меток при обработке категориальных признаков большое количество уровней для признаков категории приведет к большому количеству признаков, что может привести к многомерному пространству. Функция product_typeдобавит 21 новую функцию (22 уровня за вычетом категориального столбца product_type мы опустим) .

Мы будем использовать двоичный кодировщик с библиотекой category_encoders, чтобы избежать этих двух проблем.

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

  1. Сначала энкодер присваивает уровням порядковые целые числа
  2. Затем эти целые числа преобразуются в двоичный код
  3. Наконец, цифры этого двоичного кода разбиты на отдельные столбцы.

Например, двоичное кодирование трехуровневой категории family_status приведет к двумерному пространству, в отличие от одномерного пространства с горячим кодированием, которое всегда равно количеству уровней категории.

Построение конвейера модели и добавление шага избыточной выборки

Давайте теперь обратимся ко всему конвейеру с предварительной обработкой, которую мы обсуждали. Сначала мы оценим производительность модели, не обращая внимания на дисбаланс данных. Мы создадим конвейер модели с помощью класса scikit-learn Pipeline.

# make training and test sets
X = df1.loc[:, features]
y = df1.loc[:, target]
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
# substantiate scaler and binary encoder and define pipeline preprocessing steps
numeric_transformer = StandardScaler()
categorical_transformer = category_encoders.BinaryEncoder(cols=categorical_features)
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features + binary_features),
('cat', categorical_transformer, categorical_features)])
# define pipeline
rf_model = Pipeline(steps=[('preprocessor', preprocessor),
('classifier', RandomForestClassifier())])
rf_model.fit(X_train, y_train)
print("model score: %.3f" % rf_model.score(X_test, y_test))

оценка модели: 0,889

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

plot_roc_curve(rf_model, X_test, y_test)
plt.show()

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

print(classification_report(y_test, rf_model.predict(X_test)))

predictions = rf_model.predict(X_test)
recall = recall_score(y_test, predictions)
roc_auc = roc_auc_score(y_test, predictions)
print(f"recall: {recall:.3f}, roc_auc: {roc_auc:.3f}")

отзыв: 0,022, roc_auc: 0,507

Давайте теперь изменим наш конвейер, чтобы включить SMOTE. В библиотеке imblearn есть несколько полезных классов:

  1. Выделенный конвейер для включения SMOTE в рабочий процесс обучения модели.
  2. Класс, выполняющий передискретизацию.

Базовый класс SMOTE обрабатывает только числовые данные, поэтому, если вы не преобразуете свои категориальные признаки, используйте SMOTENC, который подходит для наборов данных с категориальные признаки. Мы будем использовать SMOTE при бинаризации наших categorical_features.

preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features + binary_features),
('cat', categorical_transformer, categorical_features)])
rf_model_smote = SMOTEpipe([('preprocessor', preprocessor),
('smote', SMOTE(random_state=42)),
('classifier', RandomForestClassifier(random_state=42))])
rf_model_smote.fit(X_train, y_train)
print(f"RF model score: {rf_model_smote.score(X_test, y_test):.3f}")

Оценка модели RF: 0,865

Хотя наша точность немного пострадала, из приведенного ниже отчета о классификации мы видим, что наша полнота для класса меньшинства выросла до 0,11.

print(classification_report(y_test, rf_model_smote.predict(X_test)))

predictions = rf_model_smote.predict(X_test)
recall = recall_score(y_test, predictions)
roc_auc = roc_auc_score(y_test, predictions)
print(f"recall: {recall:.3f}, roc_auc: {roc_auc:.3f}")

отзыв: 0,109, roc_auc: 0,532

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

feature_importance_df = pd.DataFrame({
'feature': categorical_transformer.fit_transform(X).columns,
'importance': rf_model_smote.named_steps.classifier.feature_importances_
}).sort_values(by="importance", ascending=False)
sns.barplot(y=feature_importance_df.feature,
x=feature_importance_df.importance)
plt.tick_params(axis='y', which='major', labelsize=9)
plt.show()

Мы видим, что credit_term менее важен, чем доход и credit_amount<. /em>, с ним коррелировали две другие функции. Давайте повторно запустим эту воронку, на этот раз без credit_term. Фрагмент кода ниже дает хороший обзор всего конвейера, который у нас есть.

numeric_features = ['credit_amount', 'age', 'income']
binary_features = ['having_children_flg', 'is_client']
categorical_features = ['sex', 'education', 'product_type', 'family_status', 'phone_operator', 'region']
features = numeric_features + categorical_features + binary_features
target = 'bad_client_target'
# first make training and test sets
X = df1.loc[:, features]
y = df1.loc[:, target]
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
numeric_transformer = StandardScaler()
categorical_transformer = category_encoders.BinaryEncoder(cols=categorical_features)
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features + binary_features),
('cat', categorical_transformer, categorical_features)])
rf_model_smote = SMOTEpipe([('preprocessor', preprocessor),
('smote', SMOTE(random_state=42)),
('classifier', RandomForestClassifier(random_state=42))])
rf_model_smote.fit(X_train, y_train)
print("RF model score: %.3f" % rf_model_smote.score(X_test, y_test))
predictions = rf_model_smote.predict(X_test)
recall = recall_score(y_test, predictions)
roc_auc = roc_auc_score(y_test, predictions)
print(f"recall: {recall:.3f}, roc_auc: {roc_auc:.3f}")
print(classification_report(y_test, rf_model_smote.predict(X_test)))

Оценка модели RF: 0,886

Мы до 22% вспоминаем! Помните, что мы начали с 2% отзыва, так что это впечатляющее улучшение.

Настройка параметров с помощью mlflow

Мы увидели, насколько полезна разработка признаков, предварительная обработка и правильный метод выборки для производительности модели. Еще один способ повысить скорость обучения и точность модели — настроить ее гиперпараметры. Параметры модели различаются в зависимости от модели, поэтому всегда полезно понимать, какие гиперпараметры могут повлиять на обработку моделью невидимых данных.

Давайте посмотрим на гиперпараметры модели Random Forest, которую мы только что обучили:

print(rf_model_smote.named_steps.classifier.get_params())
{'bootstrap': True, 'ccp_alpha': 0.0, 'class_weight': None, 'criterion': 'gini', 'max_depth': None, 'max_features': 'auto', 'max_leaf_nodes': None, 'max_samples': None, 'min_impurity_decrease': 0.0, 'min_impurity_split': None, 'min_samples_leaf': 1, 'min_samples_split': 2, 'min_weight_fraction_leaf': 0.0, 'n_estimators': 100, 'n_jobs': None, 'oob_score': False, 'random_state': 42, 'verbose': 0, 'warm_start': False}

Среди гиперпараметров, приведенных выше, вот те, которые потенциально могут улучшить процесс машинного обучения:

  • Максимальная глубина — это глубина каждого дерева в «лесу». Чем он глубже, тем больше у него разбиений и, следовательно, тем больше информации он собирает о данных. Тем не менее, большое количество разбиений может привести к переоснащению, поэтому мы ограничим глубину дерева 22, то есть числом функций (после двоичного кодирования и удаления credit_term).
  • Количество оценщиков — это количество деревьев в «лесу». Поскольку каждое дерево имеет подмножество функций и обучается на подмножестве данных, нет риска переобучения с большим количеством оценок. При этом большое количество деревьев замедляет обучение модели.
  • Максимальное количество функций – это максимальное количество функций, которое следует учитывать при поиске наилучшего разделения. Здесь также существует риск переобучения, но этот показатель мы можем отслеживать с помощью пользовательского интерфейса mlflow (подробнее об этом ниже).
  • Минимальное количество выборок на разделение – это минимальное количество выборок, необходимое для разделения узла. Это может быть представлено целым числом (для количества выборок) или числом с плавающей запятой (для пропорции выборок). Большее число/пропорция приводит к более ограниченной модели, что может привести к недообучению.
  • Минимальное количество образцов на лист – это минимальное количество образцов, необходимых для основания дерева (т. е. листьев). Как и в случае выборок на расщепление, большая минимальная выборка может привести к недостаточной подгонке.

Существует несколько методов настройки гиперпараметров: вы можете сделать это вручную, задав значения гиперпараметров, которые хотите протестировать, или, что еще лучше, с помощью поиска по сетке, где вы задаете диапазон значений для каждого из соответствующих гиперпараметров и циклический просмотр всех возможных перестановок значений. Более эффективным способом перебора этой перестановки является случайный поиск, когда вы случайным образом выполняете поиск по различным комбинациям значений, экономя вычисления и время, поскольку поиск не является исчерпывающим. В наших проектах мы используем байесовскую оптимизацию, то есть вероятностную модель с функцией, которая сопоставляет значения гиперпараметров с целевой функцией (например, максимизация отзыва или показатель ROC-AUC). Эта модель оптимизации более эффективна, чем сетка и случайный поиск, поскольку она оценивает вероятность того, что потенциальные значения гиперпараметров достигнут оптимума, исходя из того, как обстояли дела с ранее оцененными значениями.

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

Одна загвоздка в том, что нам нужно будет подготовить данные для mlflow, а не включать их в конвейер модели.

# substantiate the binary encoder and scaler
encoder = category_encoders.BinaryEncoder(cols=categorical_features)
numeric_transformer = StandardScaler()
# transaform categorical features as binarized columns
X_bin = encoder.fit_transform(X)
# use SMOTE for numeric/binary variables
smote = SMOTE(random_state=42)
# train-test split of transformed dataset
X_bin_train, X_bin_test, y_train, y_test = train_test_split(X_bin, y)
# create copies of X_train and X_test to perform more preprocessing/scaling on
X_pre_train = X_bin_train.copy()
X_pre_test = X_bin_test.copy()
# scale numeric features
X_pre_train.loc[:, numeric_features] = numeric_transformer.fit_transform(X_bin_train.loc[:, numeric_features])
X_pre_test.loc[:, numeric_features] = numeric_transformer.fit_transform(X_bin_test.loc[:, numeric_features])
# perform over-sampling on train set only (to avoid information leakage)
X_train_bal, y_train_bal = smote.fit_resample(X_pre_train, y_train)

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

#mlflow.sklearn.autolog()
# With autolog() enabled, all model parameters, a model score, and the fitted model are automatically logged.
with mlflow.start_run():
# Set the model parameters.
n_estimators = 100
max_depth = None
max_features = 'auto'
random_state = 42
input_schema = Schema([
ColSpec("double", "credit_amount"),
ColSpec("double", "age"),
ColSpec("double", "income"),
ColSpec("double", "sex_0"),
ColSpec("double", "sex_1"),
ColSpec("double", "education_0"),
ColSpec("double", "education_1"),
ColSpec("double", "education_2"),
ColSpec("double", "product_type_0"),
ColSpec("double", "product_type_1"),
ColSpec("double", "product_type_2"),
ColSpec("double", "product_type_3"),
ColSpec("double", "product_type_4"),
ColSpec("double", "family_status_0"),
ColSpec("double", "family_status_1"),
ColSpec("double", "phone_operator_0"),
ColSpec("double", "phone_operator_1"),
ColSpec("double", "phone_operator_2"),
ColSpec("double", "region_0"),
ColSpec("double", "region_1"),
ColSpec("double", "having_children_flg"),
ColSpec("double", "is_client")
])
# Create and train model.
rf = RandomForestClassifier(n_estimators=n_estimators, max_depth=max_depth, max_features=max_features, random_state=random_state)
rf.fit(X_train_bal, y_train_bal)
# Use the model to make predictions on the test dataset.
predictions = rf.predict(X_pre_test)
recall = recall_score(y_test, predictions)
roc_auc = roc_auc_score(y_test, predictions)
f1 = f1_score(y_test, predictions)
mlflow.sklearn.log_model(rf, "rf_base")
print(f"Recall: {recall:.3f}, ROC-AUC: {roc_auc:.3f}, F1-score: {f1:.3f}")

Отзыв: 0,289, ROC-AUC: 0,595, оценка F1: 0,25

Давайте проверим параметры этой модели:

print(rf.get_params())
{'bootstrap': True, 'ccp_alpha': 0.0, 'class_weight': None, 'criterion': 'gini', 'max_depth': None, 'max_features': 'auto', 'max_leaf_nodes': None, 'max_samples': None, 'min_impurity_decrease': 0.0, 'min_impurity_split': None, 'min_samples_leaf': 1, 'min_samples_split': 2, 'min_weight_fraction_leaf': 0.0, 'n_estimators': 100, 'n_jobs': None, 'oob_score': False, 'random_state': 42, 'verbose': 0, 'warm_start': False}

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

search_space = {
'max_depth': scope.int(hp.quniform('max_depth', 2, 22, 1)),
'n_estimators': scope.int(hp.quniform('n_estimators', 100, 2000, 100)),
'max_features': scope.int(hp.quniform('max_features', 3, 8, 1)),
'min_samples_leaf': scope.float(hp.quniform('min_samples_leaf', 0.1, 0.5, 0.1)),
'min_samples_split': scope.float(hp.quniform('min_samples_split', 0.1, 1.0, 0.1))
}

Одним из основных преимуществ mlflow является его подробный пользовательский интерфейс для просмотра экспериментов (то есть моделей с различными перестановками гиперпараметров) и их сравнения на основе нескольких показателей. Однако в функции оптимизации train_model ниже мы должны выбрать одну целевую функцию для оптимизации. Мы обсудили показатели отзыва и ROC-AUC как индикаторы чувствительности модели и компромисса между высокими показателями истинных и ложных положительных результатов. Наша задача заключалась в том, что оптимизация при высокой полноте даст модель с полнотой 1,0 и показателем ROC-AUC 0,5; то есть, хотя модель не давала ложноотрицательных результатов, она давала много ложноположительных результатов. Оценка ROC-AUC оказалась лучшей мерой для оптимизации, чтобы максимизировать чувствительность и минимизировать количество ложноположительных результатов. Обратите внимание, что мы меняем знак оценки F1, поскольку оптимизация модели mlflow направлена ​​​​на минимизацию целевой функции (обычно потери, но в этом случае F1-оценка).

def train_model(params):
rf_model_smote = SMOTEpipe([
('smote', SMOTE(random_state=42)),
('classifier', RandomForestClassifier(
random_state=42,
**params
))])
rf_model_smote.fit(X_pre_train, y_train)
predictions = rf_model_smote.predict(X_pre_test)
# Evaluate the model
recall = recall_score(y_test, predictions)
roc_auc = roc_auc_score(y_test, predictions)
f1 = f1_score(y_test, predictions)
mlflow.sklearn.log_model(rf_model_smote, "rf_smote")
#mlflow.log_params(params)
mlflow.log_metrics({
'recall': recall,
'roc_auc': roc_auc,
'f1': f1
})
return {"loss":-1*roc_auc, "recall":recall, "status": STATUS_OK}
with mlflow.start_run() as run:
best_params = fmin(
fn=train_model,
space=search_space,
algo=tpe.suggest,
max_evals=32)

100%|██████████| 32/32 [01:52‹00:00, 3,52 с/проба, лучший проигрыш: -0,6843109682603454]

best_params = hyperopt.space_eval(search_space, best_params)
best_params

{‘max_depth’: 18,
‘max_features’: 4,
‘min_samples_leaf’: 0,1,
‘min_samples_split’: 0,30000000000000004,
‘n_estimators’: 700}

rf_model_tuned = SMOTEpipe([
('smote', SMOTE(random_state=42)),
('classifier', RandomForestClassifier(
random_state=42,
**best_params
))])
rf_model_tuned.fit(X_pre_train, y_train)
predictions = rf_model_tuned.predict(X_pre_test)
# Evaluate the model
recall = recall_score(y_test, predictions)
roc_auc = roc_auc_score(y_test, predictions)
print(f"Recall: {recall:.3f}\nROC AUC score: {roc_auc:.3f}")

Отзыв: 0,605
Показатель ROC AUC: 0,684

Мы можем просмотреть запуски в пользовательском интерфейсе mlflow, запустив !mlflow ui в своем блокноте. Вы получите представление, подобное приведенному ниже, с подробной информацией о том, когда модель была запущена, а также о любых зарегистрированных метриках и параметрах. Вы увидите, что лучшая модель, оптимизированная для наивысшего показателя ROC-AUC, имеет (среди) лучшие показатели отзыва и F1.

Image('mlflow_ui.png')

print(classification_report(y_test, rf_model_tuned.predict(X_bin_test)))

поддержка точного отзыва f1-score

feature_importance_df = pd.DataFrame({
'feature': X_pre_train.columns,
'importance': rf_model_smote.named_steps.classifier.feature_importances_
}).sort_values(by="importance", ascending=False)
sns.barplot(y=feature_importance_df.feature,
x=feature_importance_df.importance)
plt.show() 

Последние мысли

В наших решениях по прикладному искусственному интеллекту и профилактическому обслуживанию нам удалось предоставить точные модели, чтобы повысить ценность для наших клиентов, уточнив каждый этап жизненного цикла машинного обучения: разработку функций, предварительную обработку, методы выборки, оптимизацию гиперпараметров и соответствующие метрики оценки модели. В игрушечном наборе данных, который мы использовали здесь, мы смогли улучшить отзыв с 2% до 67% при маркировке класса, для которого у нас мало наблюдений. Наша работа еще не сделана. Из модели, которую мы обучили, можно извлечь больше информации, исследуя влияние различных функций на прогнозы модели. Многие клиенты находят объяснение модели столь же ценным, как и отлаженный конвейер модели прогнозирования. С моделью, которая хорошо адаптирована к бизнес-вопросам и характеристикам данных, мы можем с уверенностью предоставить больше информации, соответствующей потребностям наших клиентов.