Практическое введение в калибровку модели с использованием Python

Что такое калибровка модели?

При работе с задачами классификации модели машинного обучения часто дают вероятностный результат в диапазоне от 0 до 1. Этот вероятностный результат затем используется людьми для принятия решений. К сожалению, вероятностные результаты многих моделей машинного обучения нельзя интерпретировать напрямую как вероятность того, что событие произойдет. Для достижения этого результата модель необходимо откалибровать.

Формально говорят, что модель идеально откалибрована, если для любого значения вероятности p предсказание класса с достоверностью p верно в 100*p процентах случаев. Проще говоря, вероятностный результат равен вероятности возникновения.

Представим идеальную калиброванную и некалиброванную кривую.

import numpy as np
import pandas as pd
import plotly.io as pio
import plotly.graph_objects as go
import plotly.express as px
pio.renderers.default = "plotly_mimetype+notebook_connected"

chart_df = pd.DataFrame({
    "actual_prob": np.arange(0,1.1, 0.1),
    "Calibrated": np.arange(0,1.1, 0.1),
    "Non-Calibrated":  [min(1,val - np.random.randn()*(1-idx)/20) for idx, val in enumerate(np.arange(0,1.1, 0.1))]
})
fig = px.line(
        data_frame = chart_df, 
        markers = True,
        x = "actual_prob", 
        y = ["Calibrated", "Non-Calibrated"],
        template = "plotly_dark")

fig.update_layout(
        title  = {"text": "Calibrated vs Non-calibrated model", "y": 0.98, "x": 0.5},
        xaxis_title="Predicted Probability",
        yaxis_title="Actual Probability",
        font = dict(size=15), 
        legend=dict(yanchor="bottom", y=0.95, orientation="h")
)

fig.update_traces(patch={"line": {"dash": "dash"}}) 
fig.show(renderer="notebook")

На оси x у нас есть выходные данные модели p, которые находятся между 0 и 1, а на оси y у нас есть доли положительных значений, захваченных в интервале прогнозируемой вероятности. Мы ожидаем линейную зависимость с наклоном 1.

Зачем нужна калибровка модели?

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

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

В этом случае у Продукта 1 с доходом в 10 долларов вероятность допродажи составляет 80 %, а у Продукта 2 с доходом в 100 долларов вероятность допродажи составляет 60 %. Как бизнес вы можете порекомендовать Продукт 2 покупателю из-за более высокой ожидаемой ценности.

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

Практический пример: три метода калибровки

Для демонстрации в этой статье мы будем решать задачу бинарной классификации, используя Набор данных о доходах взрослых из Репозитория машинного обучения UCI[1], чтобы предсказать, будет ли определенный индивидуальный доход на основе различной информации переписи ( уровень образования, возраст, пол, профессия и т. д.) превышает 50 000 долларов в год. В этом примере мы ограничим нашу область действия Соединенными Штатами и следующими предикторами:

  • Age: continuous переменная, возраст особей
  • Occupation: categorical переменная, Техподдержка, Кузовно-ремонтная, Прочая-услуга, Продажи, Руководящая-менеджерская, Проф-специальность, Обработчики-уборщики, Машинно-эксплуатационный осмотр, Адм-канцелярия, Сельскохозяйственно-рыболовная, Транспортно-перевозочная, Приват -дом-серв, Охран-серв, Вооруж.-Силы.
  • HoursPerWeek: переменная continuous, количество часов, проведенных на работе в неделю.
  • Education: categorical переменная, бакалавриат, некоторый колледж, 11-й, высшая степень, проф-школа, доцент, ассоц-вок, 9-й, 7-8-й, 12-й, магистр, 1-4-й, 10-й, докторантура, 5-й-6-й , дошкольное.

В приведенном ниже коде мы:

  • Импортируйте библиотеки, необходимые для этого проекта.
  • Загрузите данные из нашего источника
  • Назовите наши столбцы
  • Отфильтруйте данные только по нужным столбцам и ограничьте нашу область действия только США для простоты.
  • Метка кодирует наши категориальные столбцы
  • Установите наше случайное начальное число (для воспроизводимости)
  • Разделите наш набор данных на тест и обучение
## Importing required libraries
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn import metrics
from sklearn.isotonic import IsotonicRegression
from sklearn.preprocessing import LabelEncoder
from sklearn.calibration import CalibratedClassifierCV
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use('dark_background')

def calibration_data(y_true, y_pred):
    df = pd.DataFrame({'y_true':y_true, 'y_pred_bucket': (y_pred//0.05)*0.05 + 0.025})
    cdf = df.groupby(['y_pred_bucket'], as_index=False).agg({'y_true':["mean","count"]})
    return cdf.y_true.values[:,0][cdf.y_true.values[:,1]>10], cdf.y_pred_bucket.values[cdf.y_true.values[:,1]>10]

def label_encoder(df,columns):
    '''
    Function to label encode
    Required Input - 
        - df = Pandas DataFrame
        - columns = List input of all the columns which needs to be label encoded
    Expected Output -
        - df = Pandas DataFrame with lable encoded columns
        - le_dict = Dictionary of all the column and their label encoders
    '''
    le_dict = {}
    for c in columns:
        print("Label encoding column - {0}".format(c))
        lbl = LabelEncoder()
        lbl.fit(list(df[c].values.astype('str')))
        df[c] = lbl.transform(list(df[c].values.astype('str')))
        le_dict[c] = lbl
    return df, le_dict

df = pd.read_csv( "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data", header=None)
df.columns = [
    "Age", "WorkClass", "fnlwgt", "Education", "EducationNum",
    "MaritalStatus", "Occupation", "Relationship", "Race", "Gender",
    "CapitalGain", "CapitalLoss", "HoursPerWeek", "NativeCountry", "Income"
]

## Filtering for Unites states
df = df.loc[df.NativeCountry == ' United-States',:]

## Only - Taking required columns
df = df.loc[:,["Education", "Age","Occupation", "HoursPerWeek", "Income"]]
df, _ = label_encoder(df, columns = ["Education", "Occupation", "Income"])
X = df.loc[:,["Education","Age", "Occupation", "HoursPerWeek"]]
y = df.Income

seed = 42
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=seed)

Далее мы:

  • Подогнать к этим данным случайный лесной классификатор
  • Визуализируйте калибровочный график
## Fitting the model on training data
rf_model = RandomForestClassifier(random_state=seed)
rf_model.fit(X_train, y_train)

## getting the output to visualize on test data
prob_true, prob_pred  = calibration_data(y_true = y_test, 
                                          y_pred = rf_model.predict_proba(X_test)[:,1])
chart_df = pd.DataFrame({
    "actuals": prob_true,
    "predicted": prob_pred,
    "expected": prob_pred
})
fig = px.line(
        data_frame = chart_df, 
        markers = True,
        x = "predicted", 
        y = ["actuals", "expected"], 
        template = "plotly_dark")
fig.update_layout(
        title  = {"text": "Calibration Plot: Without calibration", "y": 0.95, "x": 0.5},
        xaxis_title="Predicted Probability",
        yaxis_title="Actual Probability",
        font = dict(size=15)
)
fig.show(renderer='notebook')

Как мы видим выше, модель завышает прогноз в нижних децилях и занижает в более высоких децилях.

Метод калибровки 1: Изотоническая регрессия

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

Математически мы можем записать изотоническую регрессию следующим образом:

Где-

  • изотоническая функция
  • f(x) — прогнозируемая вероятность исходного классификатора f
  • pcalib — калиброванная вероятность

и мы хотим оптимизировать следующую функцию -

Изотоническая регрессия склонна к переоснащению, поэтому она работает лучше, когда доступно достаточно обучающих данных.

Мы будем использовать CalibratedClassifierCV для калибровки нашего классификатора. Метод isotonic соответствует непараметрическому изотоническому регрессору, который выводит ступенчатую неубывающую функцию. [2]

## training model using random forest and isotonic regression for calibration
calibrated_rf = CalibratedClassifierCV(RandomForestClassifier(random_state=seed), method = 'isotonic')
calibrated_rf.fit(X_train, y_train)

## getting the output to visualize on test data
prob_true_calib, prob_pred_calib  = calibration_data(y_test, calibrated_rf.predict_proba(X_test)[:,1])

chart_df = pd.DataFrame({
    "actuals": prob_true_calib,
    "predicted": prob_pred_calib,
    "expected": prob_pred_calib
})
fig = px.line(
        data_frame = chart_df, 
        markers = True,
        x = "predicted", 
        y = ["actuals", "expected"], 
        template = "plotly_dark")
fig.update_layout(
        title  = {"text": "Calibration Plot: Isotonic", "y": 0.95, "x": 0.5},
        xaxis_title="Predicted Probability",
        yaxis_title="Actual Probability",
        font = dict(size=15)
)
fig.show(renderer='notebook')

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

Метод калибровки 2: Сигмоидальное/Платтовское масштабирование

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

Где -

  • pcalib — калиброванная вероятность
  • f(x) — прогнозируемая вероятность исходного классификатора f
  • A и B являются скалярными параметрами, которые изучены алгоритмом

Параметры A и B оцениваются с использованием метода максимального правдоподобия, который оптимизируется на том же обучающем наборе, что и для исходного классификатора f.

Масштабирование Sigmoid/Platt изначально было изобретено для SVM, и оно хорошо работает и для других методов. Этот метод менее подвержен переоснащению, и его следует предпочесть изотонической регрессии, если у вас меньше обучающих данных.

## training model using random forest and sigmoid method for calibration
calibrated_sigmoid = CalibratedClassifierCV(
     RandomForestClassifier(random_state=seed), method = 'sigmoid')
calibrated_sigmoid.fit(X_train, y_train)

## getting the output to visualize on test data
prob_true_calib, prob_pred_calib  = calibration_data(y_test, calibrated_sigmoid.predict_proba(X_test)[:,1])

chart_df = pd.DataFrame({
    "actuals": prob_true_calib,
    "predicted": prob_pred_calib,
    "expected": prob_pred_calib
})
fig = px.line(
        data_frame = chart_df, 
        markers = True,
        x = "predicted", 
        y = ["actuals", "expected"], 
        template = "plotly_dark")
fig.update_layout(
        title  = {"text": "Calibration Plot : Sigmoid", "y": 0.95, "x": 0.5},
        xaxis_title="Predicted Probability",
        yaxis_title="Actual Probability",
        font = dict(size=15)
)
fig.show(renderer='notebook')

Как мы видим выше, модель более откалибрована, чем наша модель по умолчанию, но не так хорошо, как метод isotonic.

Метод калибровки 3: обучение с метрикой «Logloss»

Многие модели классификаторов ансамбля на основе повышения, такие как GradientBoostingClassifier, LightGBM и Xgboost, используют log_loss в качестве функции потерь по умолчанию. Обучение с помощью log_loss помогает откалибровать выходные вероятности. Чтобы объяснить, почему потеря журнала помогает при калибровке, давайте рассмотрим функцию потери двоичного журнала:

Теперь давайте возьмем пример наблюдения, где истинный результат равен 1. Рассмотрим два прогноза модели: 0,7 и 0,9. Если мы возьмем отсечку > 0,5, оба этих прогноза верны, но логарифм потерь для прогнозов 0,7 и 0,9 составляет 0,36 и 0,1 соответственно. Как мы видим, логарифмическая потеря снижает неопределенность прогноза, заставляя модель прогнозировать результат, близкий к фактическому.

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

## Fitting the model on training data
gb_model = GradientBoostingClassifier()
gb_model.fit(X_train, y_train)

## getting the output to visualize on test data
prob_true, prob_pred  = calibration_data(y_true = y_test, 
                                          y_pred = gb_model.predict_proba(X_test)[:,1])

chart_df = pd.DataFrame({
    "actuals": prob_true,
    "predicted": prob_pred,
    "expected": prob_pred
})
fig = px.line(
        data_frame = chart_df, 
        markers = True,
        x = "predicted", 
        y = ["actuals", "expected"], 
        template = "plotly_dark")
fig.update_layout(
        title  = {"text": "Calibration Plot: Using boosting method", "y": 0.95, "x": 0.5},
        xaxis_title="Predicted Probability",
        yaxis_title="Actual Probability",
        font = dict(size=15)
)
fig.show(renderer='notebook')

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

Заключение и практическое руководство

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

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

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

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

Ааюш Агравал зарегистрирован в LinkedIn.

Ссылки

  1. Дуа, Д. и Графф, К. (2019). Репозиторий машинного обучения UCI [http://archive.ics.uci.edu/ml]. Ирвин, Калифорния: Калифорнийский университет, Школа информационных и компьютерных наук. Этот набор данных находится под лицензией Creative Commons Attribution 4.0 International (CC BY 4.0). ↩︎
  2. Scikit Learn калибровочная документация↩︎