Конформная многоквантильная регрессия с Catboost

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







Как прогнозировать интервалы, пропорциональные риску, с помощью «конформной квантильной регрессии
Этот алгоритм, опубликованный в 2019 году учеными из Стэнфорда, сочетает квантильную регрессию с конформным прогнозированием. Здесь…в направлении datascience.co»



Резюме: почему мультиквантильная регрессия?

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

Используя традиционную квантильную регрессию, для создания интервала прогнозирования 95% потребуется одна модель для 2,5-го квантиля, одна для 97,5-го квантиля и, возможно, третья для ожидаемого значения или 50-го квантиля. Один прогноз для каждой модели будет выглядеть примерно так:

Предполагая, что эти квантили откалиброваны, они раскрывают несколько идей. Во-первых, вероятность того, что цель меньше или равна 3,6, с учетом функций составляет около 0,50 или 50 %. Точно так же вероятность того, что целевое значение находится между 3,25 и 4,38, с учетом особенностей составляет примерно 0,95 или 95%.

Хотя выходные данные моделей хороши и точно соответствуют нашим требованиям, мы можем захотеть динамически корректировать нашу толерантность к риску. Например, что, если нам нужно быть более консервативным и требовать интервала прогнозирования 99%? Точно так же, что, если мы более склонны к риску и можем терпеть интервал прогнозирования 90% или 80%? Что, если нам нужны ответы на такие вопросы, как «учитывая особенности, какова вероятность того, что цель больше, чем y1?». Мы также можем задать такие вопросы, как «учитывая особенности, какова вероятность того, что цель находится между y1 и y2?». Средства многоквантильной регрессии, отвечающие на эти вопросы, предсказывают столько квантилей, сколько указано:

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

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

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

Проблема с квантильной регрессией

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

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

Конформная многоквантильная регрессия

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

Не беспокойтесь, если это покажется слишком абстрактным, шаги на самом деле просты:

  1. Создайте набор для обучения, калибровки и тестирования. Соответствуйте мультиквантильной модели на тренировочном наборе, чтобы предсказать все интересующие квантили.
  2. Делайте прогнозы по калибровочному набору. Для каждого экземпляра калибровки и прогнозируемого квантиля вычислите разницу между прогнозируемым квантилем и соответствующим целевым значением. Это показатели соответствия.
  3. Для каждого примера тестирования и прогнозируемого квантиля (скажем, q) вычтите квантиль 1-q оценок соответствия, соответствующих квантилю q, из прогнозируемого квантиля модели. Это новые предсказанные квантили.

Мы можем реализовать эту логику в классе Python:

import numpy as np
import pandas as pd
from catboost import CatBoostRegressor, CatBoostError
from typing import Iterable


class ConformalMultiQuantile(CatBoostRegressor):
    
    def __init__(self, quantiles:Iterable[float], *args, **kwargs):

        """
        Initialize a ConformalMultiQuantile object.

        Parameters
        ----------
        quantiles : Iterable[float]
            The list of quantiles to use in multi-quantile regression.
        *args
            Variable length argument list.
        **kwargs
            Arbitrary keyword arguments.
        """

        kwargs['loss_function'] = self.create_loss_function_str(quantiles)
        super().__init__(*args, **kwargs)
        self.quantiles = quantiles
        self.calibration_adjustments = None
           
        
    @staticmethod
    def create_loss_function_str(quantiles:Iterable[float]):

        """
        Format the quantiles as a string for Catboost

        Paramters
        ---------
        quantiles : Union[float, List[float]]
            A float or list of float quantiles
        
        Returns
        -------
        The loss function definition for multi-quantile regression
        """

        quantile_str = str(quantiles).replace('[','').replace(']','')

        return f'MultiQuantile:alpha={quantile_str}'
                    
    def calibrate(self, x_cal, y_cal):

        """
        Calibrate the multi-quantile model

        Paramters
        ---------
        x_cal : ndarray
            Calibration inputs
        y_cal : ndarray
            Calibration target
        """

        # Ensure the model is fitted
        if not self.is_fitted():

            raise CatBoostError('There is no trained model to use calibrate(). Use fit() to train model. Then use this method.')
        
        # Make predictions on the calibration set
        uncalibrated_preds = self.predict(x_cal)

        # Compute the difference between the uncalibrated predicted quantiles and the target
        conformity_scores = uncalibrated_preds - np.array(y_cal).reshape(-1, 1)
        
        # Store the 1-q quantile of the conformity scores
        self.calibration_adjustments = \
            np.array([np.quantile(conformity_scores[:,i], 1-q) for i,q in enumerate(self.quantiles)])
        
    def predict(self, data, prediction_type=None, ntree_start=0, ntree_end=0, thread_count=-1, verbose=None, task_type="CPU"):

        """
        Predict using the trained model.

        Parameters
        ----------
        data : pandas.DataFrame or numpy.ndarray
            Data to make predictions on
        prediction_type : str, optional
            Type of prediction result, by default None
        ntree_start : int, optional
            Number of trees to start prediction from, by default 0
        ntree_end : int, optional
            Number of trees to end prediction at, by default 0
        thread_count : int, optional
            Number of parallel threads to use, by default -1
        verbose : bool or int, optional
            Verbosity, by default None
        task_type : str, optional
            Type of task, by default "CPU"

        Returns
        -------
        numpy.ndarray
            The predicted values for the input data.
        """
        
        preds = super().predict(data, prediction_type, ntree_start, ntree_end, thread_count, verbose, task_type)

        # Adjust the predicted quantiles according to the quantiles of the
        # conformity scores
        if self.calibration_adjustments is not None:

            preds = preds - self.calibration_adjustments

        return preds

Пример: набор данных по сверхпроводимости

Мы реализуем конформную многоквантильную регрессию для набора данных о сверхпроводимости, доступного в репозитории машинного обучения UCI. Этот набор данных содержит 21 263 экземпляра 81 сверхпроводниковой характеристики с их критической температурой (целевой). Данные разделены таким образом, что ~64% выделяется для обучения, ~16% — для калибровки и 20% — для тестирования.

# Dependencies
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from catboost import CatBoostRegressor, CatBoostError
from sklearn.model_selection import train_test_split
from typing import Iterable
pd.set_option('display.max.columns', None)
sns.set()

# Read in superconductivity dataset
data = pd.read_csv('train.csv')

# Predicting critical temperature
target = 'critical_temp'

# 80/20 train/test split
x_train, x_test, y_train, y_test = train_test_split(data.drop(target, axis=1), data[target], test_size=0.20)

# Hold out 20% of the training data for calibration
x_train, x_cal, y_train, y_cal = train_test_split(x_train, y_train, test_size=0.20)

print("Training shape:", x_train.shape) # Training shape: (13608, 81)
print("Calibration shape:", x_cal.shape) # Calibration shape: (3402, 81)
print("Testing shape:", x_test.shape) # Testing shape: (4253, 81)

Мы укажем набор квантилей для прогнозирования. Чтобы проиллюстрировать силу мультиквантильной регрессии, модель будет предсказывать 200 квантилей от 0,005 до 0,99 — на практике это, вероятно, немного избыточно. Затем мы сопоставим конформную многоквантильную модель, сделаем некалиброванные прогнозы, откалибруем модель на калибровочном наборе. strong> и делать калиброванные прогнозы.

# Store quantiles 0.005 through 0.99 in a list
quantiles = [q/200 for q in range(1, 200)]

# Instantiate the conformal multi-quantile model
conformal_model = ConformalMultiQuantile(iterations=100,
                                        quantiles=quantiles,
                                        verbose=10)

# Fit the conformal multi-quantile model
conformal_model.fit(x_train, y_train)

# Get predictions before calibration
preds_uncalibrated = conformal_model.predict(x_test)
preds_uncalibrated = pd.DataFrame(preds_uncalibrated, columns=[f'pred_{q}' for q in quantiles])

# Calibrate the model
conformal_model.calibrate(x_cal, y_cal)

# Get calibrated predictions
preds_calibrated = conformal_model.predict(x_test)
preds_calibrated = pd.DataFrame(preds_calibrated, columns=[f'pred_{q}' for q in quantiles])

preds_calibrated.head()

Полученные прогнозы должны выглядеть примерно так:

На тестовом наборе мы можем измерить, насколько хорошо некалиброванные и калиброванные прогнозы согласуются с вероятностью левого хвоста, которую они должны представлять. Например, если квантили откалиброваны, 40% целевых значений должны быть меньше или равны прогнозируемому квантилю 0,40, 90% целевых значений должны быть меньше или равны прогнозируемому квантилю 0,90 и т. д. Приведенный ниже код вычисляет среднее абсолютное значение. ошибка (MAE) между желаемой вероятностью левого хвоста и фактической вероятностью левого хвоста, охватываемой предсказанными квантилями:

# Initialize an empty DataFrame
comparison_df = pd.DataFrame()

# For each predicted quantile
for i, quantile in enumerate(quantiles):
    
    # Compute the proportion of testing observations that were less than or equal 
    # to the uncalibrated predicted quantile
    actual_prob_uncal = np.mean(y_test.values <= preds_uncalibrated[f'pred_{quantile}'])

    # Compute the proportion of testing observations that were less than or equal 
    # to the calibrated predicted quantile
    actual_prob_cal = np.mean(y_test.values <= preds_calibrated[f'pred_{quantile}'])
    
    comparison_df_curr = pd.DataFrame({
                                    'desired_probability':quantile,
                                    'actual_uncalibrated_probability':actual_prob_uncal,
                                    'actual_calibrated_probability':actual_prob_cal}, index=[i])

    comparison_df = pd.concat([comparison_df, comparison_df_curr])

comparison_df['abs_diff_uncal'] = (comparison_df['desired_probability'] - comparison_df['actual_uncalibrated_probability']).abs()
comparison_df['abs_diff_cal'] = (comparison_df['desired_probability'] - comparison_df['actual_calibrated_probability']).abs()

print("Uncalibrated quantile MAE:", comparison_df['abs_diff_uncal'].mean()) 
print("Calibrated quantile MAE:", comparison_df['abs_diff_cal'].mean()) 

# Uncalibrated quantile MAE: 0.02572999018133225
# Calibrated quantile MAE: 0.007850550660662823

Некалиброванные квантили отличались в среднем примерно на 0,026, а калиброванные квантили — на 0,008. Следовательно, откалиброванные квантили больше соответствовали желаемым вероятностям левого хвоста.

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

coverage_df = pd.DataFrame()

for i, alpha in enumerate(np.arange(0.01, 0.41, 0.01)):

    lower_quantile = round(alpha/2, 3)
    upper_quantile = round(1 - alpha/2, 3)
    
    # Compare actual to expected coverage for both models
    lower_prob_uncal = comparison_df[comparison_df['desired_probability'] == lower_quantile]['actual_uncalibrated_probability'].values[0]
    upper_prob_uncal = comparison_df[comparison_df['desired_probability'] == upper_quantile]['actual_uncalibrated_probability'].values[0]

    lower_prob_cal = comparison_df[comparison_df['desired_probability'] == lower_quantile]['actual_calibrated_probability'].values[0]
    upper_prob_cal = comparison_df[comparison_df['desired_probability'] == upper_quantile]['actual_calibrated_probability'].values[0]

    coverage_df_curr = pd.DataFrame({'desired_coverage':1-alpha,
                                    'actual_uncalibrated_coverage':upper_prob_uncal - lower_prob_uncal,
                                    'actual_calibrated_coverage':upper_prob_cal - lower_prob_cal}, index=[i])
    
    coverage_df = pd.concat([coverage_df, coverage_df_curr])


coverage_df['abs_diff_uncal'] = (coverage_df['desired_coverage'] - coverage_df['actual_uncalibrated_coverage']).abs()
coverage_df['abs_diff_cal'] = (coverage_df['desired_coverage'] - coverage_df['actual_calibrated_coverage']).abs()

print("Uncalibrated Coverage MAE:", coverage_df['abs_diff_uncal'].mean()) 
print("Calibrated Coverage MAE:", coverage_df['abs_diff_cal'].mean()) 

# Uncalibrated Coverage MAE: 0.03660674817775689
# Calibrated Coverage MAE: 0.003543616270867622

fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(coverage_df['desired_coverage'],
        coverage_df['desired_coverage'],
        label='Perfect Calibration')
ax.scatter(coverage_df['desired_coverage'],
           coverage_df['actual_uncalibrated_coverage'],
           color='orange',
           label='Uncalibrated Model')
ax.scatter(coverage_df['desired_coverage'],
           coverage_df['actual_calibrated_coverage'],
           color='green',
           label='Calibrated Model')

ax.set_xlabel('Desired Coverage')
ax.set_ylabel('Actual Coverage')
ax.set_title('Desired vs Actual Coverage')
ax.legend()
plt.show()

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

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

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

# Fit a model using the training and calibration data
regular_model = ConformalMultiQuantile(iterations=100,
                                        quantiles=quantiles,
                                        verbose=10)

regular_model.fit(pd.concat([x_train, x_cal]), pd.concat([y_train, y_cal]))


# Fit a model on the training data only
conformal_model = ConformalMultiQuantile(iterations=100,
                                        quantiles=quantiles,
                                        verbose=10)

conformal_model.fit(x_train, y_train)

# Get predictions before calibration
preds_uncalibrated = regular_model.predict(x_test)
preds_uncalibrated = pd.DataFrame(preds_uncalibrated, columns=[f'pred_{q}' for q in quantiles])

# Calibrate the model
conformal_model.calibrate(x_cal, y_cal)

# Get calibrated predictions
preds_calibrated = conformal_model.predict(x_test)
preds_calibrated = pd.DataFrame(preds_calibrated, columns=[f'pred_{q}' for q in quantiles])

comparison_df = pd.DataFrame()

# Compare actual to predicted left-tailed probabilities
for i, quantile in enumerate(quantiles):
 
    actual_prob_uncal = np.mean(y_test.values <= preds_uncalibrated[f'pred_{quantile}'])
    actual_prob_cal = np.mean(y_test.values <= preds_calibrated[f'pred_{quantile}'])
    
    comparison_df_curr = pd.DataFrame({
                                    'desired_probability':quantile,
                                    'actual_uncalibrated_probability':actual_prob_uncal,
                                    'actual_calibrated_probability':actual_prob_cal}, index=[i])

    comparison_df = pd.concat([comparison_df, comparison_df_curr])

comparison_df['abs_diff_uncal'] = (comparison_df['desired_probability'] - comparison_df['actual_uncalibrated_probability']).abs()
comparison_df['abs_diff_cal'] = (comparison_df['desired_probability'] - comparison_df['actual_calibrated_probability']).abs()

print("Uncalibrated quantile MAE:", comparison_df['abs_diff_uncal'].mean()) 
print("Calibrated quantile MAE:", comparison_df['abs_diff_cal'].mean()) 

# Uncalibrated quantile MAE: 0.023452756375340143
# Calibrated quantile MAE: 0.0061827359227361834

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

from sklearn.metrics import r2_score, mean_absolute_error

print(f"Uncalibrated R2 Score: {r2_score(y_test, preds_uncalibrated.mean(axis=1))}")
print(f"Calibrated R2 Score: {r2_score(y_test, preds_calibrated.mean(axis=1))} \n")

print(f"Uncalibrated MAE: {mean_absolute_error(y_test, preds_uncalibrated.mean(axis=1))}")
print(f"Calibrated MAE: {mean_absolute_error(y_test, preds_calibrated.mean(axis=1))} \n")

# Uncalibrated R2 Score: 0.8060126144892599
# Calibrated R2 Score: 0.8053382438575666 

# Uncalibrated MAE: 10.622258046774979
# Calibrated MAE: 10.557269513856014 

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

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

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

Стать участником: https://harrisonfhoffman.medium.com/membership

Рекомендации

  1. Функции потерь Catboost —https://catboost.ai/en/docs/concepts/loss-functions-regression#MultiQuantile
  2. Конформная квантильная регрессияhttps://arxiv.org/pdf/1905.03222.pdf
  3. Конформное предсказание за пределами возможности обменаhttps://arxiv.org/pdf/2202.13415.pdf
  4. Набор данных по сверхпроводимостиhttps://archive.ics.uci.edu/ml/datasets/Superconductivty+Data
  5. Как прогнозировать интервалы, пропорциональные риску, с помощью конформной квантильной регрессииhttps://towardsdatascience.com/how-to-predict-risk-proportional-intervals-with-conformal-quantile-regression-175775840dc4
  6. Как прогнозировать распределения полной вероятности с помощью конформного прогнозирования машинного обученияhttps://valeman.medium.com/how-to-predict-full-probability-distribution-using-machine-learning-conformal- прогнозирующий-f8f4d805e420