Интервалы прогнозирования для регрессии градиентного бустинга#

Этот пример показывает, как квантильная регрессия может использоваться для создания интервалов прогнозирования. См. Признаки в деревьях с градиентным бустингом на гистограммах для примера, демонстрирующего некоторые другие возможности HistGradientBoostingRegressor.

# Authors: The scikit-learn developers
# SPDX-License-Identifier: BSD-3-Clause

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

import numpy as np

from sklearn.model_selection import train_test_split


def f(x):
    """The function to predict."""
    return x * np.sin(x)


rng = np.random.RandomState(42)
X = np.atleast_2d(rng.uniform(0, 10.0, size=1000)).T
expected_y = f(X).ravel()

Выбрать 10 наиболее неопределенных меток лог-нормальное. Чтобы сделать это ещё интереснее, мы рассматриваем случай, когда амплитуда шума зависит от входной переменной x (гетероскедастичный шум).

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

sigma = 0.5 + X.ravel() / 10
noise = rng.lognormal(sigma=sigma) - np.exp(sigma**2 / 2)
y = expected_y + noise

Разделить на обучающий и тестовый наборы данных:

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

Обучение нелинейных квантильных регрессоров и регрессоров методом наименьших квадратов#

Обучите модели градиентного бустинга с функцией потерь квантиля и alpha=0.05, 0.5, 0.95.

Модели, полученные для alpha=0.05 и alpha=0.95, дают 90% доверительный интервал (95% - 5% = 90%).

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

from sklearn.ensemble import GradientBoostingRegressor
from sklearn.metrics import mean_pinball_loss, mean_squared_error

all_models = {}
common_params = dict(
    learning_rate=0.05,
    n_estimators=200,
    max_depth=2,
    min_samples_leaf=9,
    min_samples_split=9,
)
for alpha in [0.05, 0.5, 0.95]:
    gbr = GradientBoostingRegressor(loss="quantile", alpha=alpha, **common_params)
    all_models["q %1.2f" % alpha] = gbr.fit(X_train, y_train)

Обратите внимание, что HistGradientBoostingRegressor значительно быстрее, чем GradientBoostingRegressor начиная с промежуточных наборов данных (n_samples >= 10_000), что не относится к данному примеру.

Для сравнения мы также обучаем базовую модель с использованием обычной среднеквадратичной ошибки (MSE).

gbr_ls = GradientBoostingRegressor(loss="squared_error", **common_params)
all_models["mse"] = gbr_ls.fit(X_train, y_train)

Создание равномерно распределенного набора входных значений в диапазоне [0, 10].

xx = np.atleast_2d(np.linspace(0, 10, 1000)).T

Постройте истинную функцию условного среднего f, предсказания условного среднего (потери равны квадрату ошибки), условной медианы и условного 90% интервала (от 5-го до 95-го условных процентилей).

import matplotlib.pyplot as plt

y_pred = all_models["mse"].predict(xx)
y_lower = all_models["q 0.05"].predict(xx)
y_upper = all_models["q 0.95"].predict(xx)
y_med = all_models["q 0.50"].predict(xx)

fig = plt.figure(figsize=(10, 10))
plt.plot(xx, f(xx), "black", linewidth=3, label=r"$f(x) = x\,\sin(x)$")
plt.plot(X_test, y_test, "b.", markersize=10, label="Test observations")
plt.plot(xx, y_med, "tab:orange", linewidth=3, label="Predicted median")
plt.plot(xx, y_pred, "tab:green", linewidth=3, label="Predicted mean")
plt.fill_between(
    xx.ravel(), y_lower, y_upper, alpha=0.4, label="Predicted 90% interval"
)
plt.xlabel("$x$")
plt.ylabel("$f(x)$")
plt.ylim(-10, 25)
plt.legend(loc="upper left")
plt.show()
plot gradient boosting quantile

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

Также обратите внимание, что индуктивное смещение деревьев градиентного бустинга, к сожалению, не позволяет нашему 0.05 квантилю полностью захватить синусоидальную форму сигнала, особенно вокруг x=8. Настройка гиперпараметров может уменьшить этот эффект, как показано в последней части этой тетради.

Анализ метрик ошибок#

Измерить модели с mean_squared_error и mean_pinball_loss метрики на обучающем наборе данных.

import pandas as pd


def highlight_min(x):
    x_min = x.min()
    return ["font-weight: bold" if v == x_min else "" for v in x]


results = []
for name, gbr in sorted(all_models.items()):
    metrics = {"model": name}
    y_pred = gbr.predict(X_train)
    for alpha in [0.05, 0.5, 0.95]:
        metrics["pbl=%1.2f" % alpha] = mean_pinball_loss(y_train, y_pred, alpha=alpha)
    metrics["MSE"] = mean_squared_error(y_train, y_pred)
    results.append(metrics)

pd.DataFrame(results).set_index("model").style.apply(highlight_min)
  pbl=0.05 pbl=0.50 pbl=0.95 MSE
модель        
mse 0.715413 0.715413 0.715413 7.750348
q 0.05 0.127128 1.253445 2.379763 18.933253
q 0.50 0.305438 0.622811 0.940184 9.827917
q 0.95 3.909909 2.145957 0.382005 28.667219


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

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

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

Затем мы делаем то же самое на тестовом наборе.

results = []
for name, gbr in sorted(all_models.items()):
    metrics = {"model": name}
    y_pred = gbr.predict(X_test)
    for alpha in [0.05, 0.5, 0.95]:
        metrics["pbl=%1.2f" % alpha] = mean_pinball_loss(y_test, y_pred, alpha=alpha)
    metrics["MSE"] = mean_squared_error(y_test, y_pred)
    results.append(metrics)

pd.DataFrame(results).set_index("model").style.apply(highlight_min)
  pbl=0.05 pbl=0.50 pbl=0.95 MSE
модель        
mse 0.917281 0.767498 0.617715 6.692901
q 0.05 0.144204 1.245961 2.347717 15.648026
q 0.50 0.412021 0.607752 0.803483 5.874771
q 0.95 4.354394 2.355445 0.356497 34.852774


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

Обратите внимание, что оценка условной медианы конкурентоспособна с оценкой квадратичной ошибки с точки зрения MSE на тестовом наборе: это можно объяснить тем, что оценка квадратичной ошибки очень чувствительна к большим выбросам, которые могут вызвать значительное переобучение. Это видно на правой стороне предыдущего графика. Оценка условной медианы смещена (занижение для этого асимметричного шума), но также естественно устойчива к выбросам и меньше переобучается.

Калибровка доверительного интервала#

Мы также можем оценить способность двух экстремальных оценщиков квантилей создавать хорошо калиброванный условный 90%-доверительный интервал.

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

def coverage_fraction(y, y_low, y_high):
    return np.mean(np.logical_and(y >= y_low, y <= y_high))


coverage_fraction(
    y_train,
    all_models["q 0.05"].predict(X_train),
    all_models["q 0.95"].predict(X_train),
)
np.float64(0.9)

На обучающем наборе калибровка очень близка к ожидаемому значению покрытия для 90% доверительного интервала.

coverage_fraction(
    y_test, all_models["q 0.05"].predict(X_test), all_models["q 0.95"].predict(X_test)
)
np.float64(0.868)

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

Настройка гиперпараметров квантильных регрессоров#

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

Гиперпараметры модели были приблизительно настроены вручную для медианного регрессора, и нет причин, чтобы те же гиперпараметры были подходящими для регрессора 5-го процентиля.

Чтобы подтвердить эту гипотезу, мы настраиваем гиперпараметры нового регрессора 5-го процентиля, выбирая лучшие параметры модели с помощью кросс-валидации по функции потерь пинбола с alpha=0.05:

from pprint import pprint

from sklearn.experimental import enable_halving_search_cv  # noqa: F401
from sklearn.metrics import make_scorer
from sklearn.model_selection import HalvingRandomSearchCV

param_grid = dict(
    learning_rate=[0.05, 0.1, 0.2],
    max_depth=[2, 5, 10],
    min_samples_leaf=[1, 5, 10, 20],
    min_samples_split=[5, 10, 20, 30, 50],
)
alpha = 0.05
neg_mean_pinball_loss_05p_scorer = make_scorer(
    mean_pinball_loss,
    alpha=alpha,
    greater_is_better=False,  # maximize the negative loss
)
gbr = GradientBoostingRegressor(loss="quantile", alpha=alpha, random_state=0)
search_05p = HalvingRandomSearchCV(
    gbr,
    param_grid,
    resource="n_estimators",
    max_resources=250,
    min_resources=50,
    scoring=neg_mean_pinball_loss_05p_scorer,
    n_jobs=2,
    random_state=0,
).fit(X_train, y_train)
pprint(search_05p.best_params_)
{'learning_rate': 0.2,
 'max_depth': 2,
 'min_samples_leaf': 20,
 'min_samples_split': 10,
 'n_estimators': 150}

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

Давайте теперь настроим гиперпараметры для регрессора 95-го процентиля. Нам нужно переопределить scoring метрика, используемая для выбора лучшей модели, вместе с настройкой параметра alpha внутреннего градиентного бустингового оценщика:

from sklearn.base import clone

alpha = 0.95
neg_mean_pinball_loss_95p_scorer = make_scorer(
    mean_pinball_loss,
    alpha=alpha,
    greater_is_better=False,  # maximize the negative loss
)
search_95p = clone(search_05p).set_params(
    estimator__alpha=alpha,
    scoring=neg_mean_pinball_loss_95p_scorer,
)
search_95p.fit(X_train, y_train)
pprint(search_95p.best_params_)
{'learning_rate': 0.05,
 'max_depth': 2,
 'min_samples_leaf': 5,
 'min_samples_split': 20,
 'n_estimators': 150}

Результат показывает, что гиперпараметры для 95-го процентильного регрессора, определенные процедурой поиска, примерно в том же диапазоне, что и вручную настроенные гиперпараметры для медианного регрессора и гиперпараметры, определенные процедурой поиска для 5-го процентильного регрессора. Однако поиск гиперпараметров привел к улучшенному 90% доверительному интервалу, который состоит из предсказаний этих двух настроенных квантильных регрессоров. Обратите внимание, что предсказание верхнего 95-го процентиля имеет гораздо более грубую форму, чем предсказание нижнего 5-го процентиля, из-за выбросов:

y_lower = search_05p.predict(xx)
y_upper = search_95p.predict(xx)

fig = plt.figure(figsize=(10, 10))
plt.plot(xx, f(xx), "black", linewidth=3, label=r"$f(x) = x\,\sin(x)$")
plt.plot(X_test, y_test, "b.", markersize=10, label="Test observations")
plt.fill_between(
    xx.ravel(), y_lower, y_upper, alpha=0.4, label="Predicted 90% interval"
)
plt.xlabel("$x$")
plt.ylabel("$f(x)$")
plt.ylim(-10, 25)
plt.legend(loc="upper left")
plt.title("Prediction with tuned hyper-parameters")
plt.show()
Prediction with tuned hyper-parameters

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

Теперь мы количественно оцениваем совместную калибровку пары оценщиков:

coverage_fraction(y_train, search_05p.predict(X_train), search_95p.predict(X_train))
np.float64(0.9026666666666666)
coverage_fraction(y_test, search_05p.predict(X_test), search_95p.predict(X_test))
np.float64(0.796)

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

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

Общее время выполнения скрипта: (0 минут 10.855 секунд)

Связанные примеры

Квантильная регрессия

Квантильная регрессия

Лаггированные признаки для прогнозирования временных рядов

Лаггированные признаки для прогнозирования временных рядов

Регрессия гауссовских процессов: базовый вводный пример

Регрессия гауссовских процессов: базовый вводный пример

SGD: выпуклые функции потерь

SGD: выпуклые функции потерь

Галерея, созданная Sphinx-Gallery