Конформная многоквантильная регрессия с 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% наблюдений на самом деле будут меньше или равны прогнозу. Это проблематично в приложениях с высоким риском, где для принятия важных решений требуются точные представления вероятностей.
Квантильная регрессия также может давать интервалы прогнозирования, которые являются слишком консервативными и впоследствии неинформативными. Как правило, интервалы прогнозирования должны быть как можно более узкими, сохраняя при этом желаемый уровень охвата.
Конформная многоквантильная регрессия
Идея конформной квантильной регрессии состоит в том, чтобы скорректировать предсказанные квантили, чтобы точно отразить желаемую устойчивость к риску и длину интервала. Это достигается с помощью шага калибровки, на котором вычисляются показатели соответствия для корректировки предсказанных квантилей. Подробнее о конформной квантильной регрессии можно прочитать в этой статье и этой статье. Для конформной многоквантильной регрессии мы будем использовать следующую теорему:
Не беспокойтесь, если это покажется слишком абстрактным, шаги на самом деле просты:
- Создайте набор для обучения, калибровки и тестирования. Соответствуйте мультиквантильной модели на тренировочном наборе, чтобы предсказать все интересующие квантили.
- Делайте прогнозы по калибровочному набору. Для каждого экземпляра калибровки и прогнозируемого квантиля вычислите разницу между прогнозируемым квантилем и соответствующим целевым значением. Это показатели соответствия.
- Для каждого примера тестирования и прогнозируемого квантиля (скажем, 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
Рекомендации
- Функции потерь Catboost —https://catboost.ai/en/docs/concepts/loss-functions-regression#MultiQuantile
- Конформная квантильная регрессия — https://arxiv.org/pdf/1905.03222.pdf
- Конформное предсказание за пределами возможности обмена — https://arxiv.org/pdf/2202.13415.pdf
- Набор данных по сверхпроводимости — https://archive.ics.uci.edu/ml/datasets/Superconductivty+Data
- Как прогнозировать интервалы, пропорциональные риску, с помощью конформной квантильной регрессии — https://towardsdatascience.com/how-to-predict-risk-proportional-intervals-with-conformal-quantile-regression-175775840dc4
- Как прогнозировать распределения полной вероятности с помощью конформного прогнозирования машинного обучения — https://valeman.medium.com/how-to-predict-full-probability-distribution-using-machine-learning-conformal- прогнозирующий-f8f4d805e420