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

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

Калибровка часто применяется после обучения модели, чтобы улучшить ее оценки вероятности. Вот пример проблемы мультиклассовой классификации с использованием набора данных Iris, который применяет калибровку в конце с использованием класса CalibratedClassifierCV из scikit-learn:

import numpy as np
from sklearn.datasets import fetch_openml
from sklearn.ensemble import RandomForestClassifier
from sklearn.calibration import CalibratedClassifierCV
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report, log_loss

# Load the "mnist_784" dataset (a subset will be used for faster execution)
mnist = fetch_openml('mnist_784')
X, y = mnist['data'], mnist['target']

# Use a subset of the dataset for faster execution
X = X[:10000]
y = y[:10000]

# Split the dataset into training and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Train a RandomForest classifier
rf = RandomForestClassifier(n_estimators=50, random_state=42)
rf.fit(X_train, y_train)

# Evaluate the classifier without calibration
y_pred = rf.predict(X_test)
y_pred_proba = rf.predict_proba(X_test)
print("Accuracy without calibration:", accuracy_score(y_test, y_pred))
print("Log loss without calibration:", log_loss(y_test, y_pred_proba))

# Calibrate the classifier using isotonic regression
calibrated_rf = CalibratedClassifierCV(rf, method='isotonic', cv=5)
calibrated_rf.fit(X_train, y_train)

# Evaluate the calibrated classifier
y_pred_calibrated = calibrated_rf.predict(X_test)
y_pred_proba_calibrated = calibrated_rf.predict_proba(X_test)
print("Accuracy with calibration:", accuracy_score(y_test, y_pred_calibrated))
print("Log loss with calibration:", log_loss(y_test, y_pred_proba_calibrated))

Accuracy without calibration: 0.9495
Log loss without calibration: 0.37637322941353757
Accuracy with calibration: 0.9525
Log loss with calibration: 0.18414947220067412

Теперь давайте оценим качество калибровки по шкале Бриера.

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

Для задачи бинарной классификации с двумя классами, A и B, показатель Бриера можно рассчитать следующим образом:

Оценка Брайера = (1/N) * Σ(Pi — Oi)²

где N — количество выборок, Pi — прогнозируемая вероятность класса A для выборки i, а Oi — фактический результат для выборки i (1, если выборка принадлежит классу A, 0, если она принадлежит классу B).

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

Для задач мультиклассовой классификации оценка Бриера рассчитывается как среднее среднеквадратичное различие между прогнозируемыми вероятностями и фактическими результатами для каждого класса. Вы можете использовать функцию scikit-learn label_binarize, чтобы преобразовать целевую переменную в двоичный формат, а затем рассчитать оценку Бриера для каждого класса.

# Binarize the true labels
y_test_binarized = label_binarize(y_test, classes=np.unique(y_test))

# Calculate the Brier score for each class
brier_scores = [
    brier_score_loss(y_test_binarized[:, i], y_pred_proba_calibrated[:, i])
    for i in range(y_test_binarized.shape[1])
]

# Calculate the average Brier score across all classes
average_brier_score = np.mean(brier_scores)

print(f"Average Brier score across all classes: {average_brier_score:.4f}")
Average Brier score across all classes: 0.0075

Здесь мы использовали «изотонический» метод внутри функции калибровки; CalibratedClassifier, а какие возможны способы.

Методы калибровки

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

  1. Масштабирование Платта (или метод Платта): этот метод включает обучение модели логистической регрессии на предсказанных вероятностях (или значениях функции принятия решения) и истинных метках. Модель логистической регрессии сопоставляет выходные данные базового классификатора с вероятностями. Он обычно используется с машинами опорных векторов и нейронными сетями.
  2. Изотоническая регрессия: этот непараметрический метод оценивает кусочно-постоянную неубывающую функцию, которая отображает предсказанные вероятности в калиброванные вероятности. Он может быть более гибким, чем Platt Scaling, но может также соответствовать данным калибровки, особенно если набор данных небольшой.
  3. Температурное масштабирование: этот метод в основном используется для калибровки нейронных сетей. Это включает в себя изучение одного параметра температуры, который регулирует вывод softmax. Прогнозируемые вероятности делятся на значение температуры, и снова применяется функция softmax.
  4. Байесовское объединение в квантили (BBQ): этот метод предполагает, что предсказанные вероятности разбиты на ячейки, а истинные вероятности в каждой ячейке подчиняются бета-распределению. BBQ оценивает параметры бета-распределения для каждого бина, используя эмпирические вероятности.
  5. Калибровка с перекрестной проверкой: во избежание переобучения калибровку можно выполнять с помощью перекрестной проверки. Набор данных разделен на k сгибов, и каждый сгиб используется для калибровки, а другие (k-1) сгибы используются для обучения классификатора.
  6. Калибровка ансамбля. Методы ансамбля, такие как бэггинг или бустинг, могут использоваться для улучшения калибровки отдельных моделей. Каждая модель в ансамбле калибруется отдельно, а их выходные данные объединяются для получения калиброванного прогноза ансамбля.
  7. Обучение с учетом затрат: в этом подходе алгоритм обучения модифицируется, чтобы учитывать затраты на неправильную классификацию. Это может помочь улучшить калибровку, сделав классификатор более консервативным в прогнозировании вероятностей.

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

Ниже приведены примеры кода Python, демонстрирующие, как применять каждый метод калибровки с использованием библиотеки scikit-learn для задачи двоичной классификации.

Во-первых, давайте создадим синтетический набор данных и обучим машину опорных векторов (SVM) в качестве базового классификатора:

import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.metrics import brier_score_loss

# Create synthetic dataset
X, y = make_classification(n_samples=1000, n_features=20, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Train an SVM classifier
base_classifier = SVC(kernel='linear', C=1, probability=True)
base_classifier.fit(X_train, y_train)

# Get predicted probabilities
y_pred_probs = base_classifier.predict_proba(X_test)[:, 1]

# Evaluate Brier score
brier_base = brier_score_loss(y_test, y_pred_probs)
print(f"Brier score of base classifier: {brier_base:.4f}")
Brier score of base classifier: 0.1090

Теперь применим каждый метод калибровки:

Платт Масштабирование

from sklearn.calibration import CalibratedClassifierCV

# Calibrate using Platt Scaling
platt_calibrator = CalibratedClassifierCV(base_classifier, method='sigmoid', cv='prefit')
platt_calibrator.fit(X_train, y_train)

# Get Platt calibrated probabilities
y_pred_probs_platt = platt_calibrator.predict_proba(X_test)[:, 1]

# Evaluate Brier score
brier_platt = brier_score_loss(y_test, y_pred_probs_platt)
print(f"Brier score after Platt Scaling: {brier_platt:.4f}"Brier score after Platt Scaling: 0.1091
Brier score after Platt Scaling: 0.1091

Изотоническая регрессия

# Calibrate using Isotonic Regression
isotonic_calibrator = CalibratedClassifierCV(base_classifier, method='isotonic', cv='prefit')
isotonic_calibrator.fit(X_train, y_train)

# Get isotonic calibrated probabilities
y_pred_probs_isotonic = isotonic_calibrator.predict_proba(X_test)[:, 1]

# Evaluate Brier score
brier_isotonic = brier_score_loss(y_test, y_pred_probs_isotonic)
print(f"Brier score after Isotonic Regression: {brier_isotonic:.4f}")
Brier score after Isotonic Regression: 0.1071

Масштабирование температуры

Этот метод недоступен напрямую в scikit-learn, но может быть реализован следующим образом:

from scipy.optimize import minimize_scalar
from sklearn.metrics import log_loss

def temperature_scaling(logits, y):
    def temperature_obj(t):
        temp_logits = logits / t
        return log_loss(y, temp_logits)
    
    res = minimize_scalar(temperature_obj)
    return logits / res.x

# Get logits for the test set
logits = base_classifier.decision_function(X_test)

# Calibrate using Temperature Scaling
logits_temp_scaled = temperature_scaling(logits, y_test)
y_pred_probs_temp_scaled = np.exp(logits_temp_scaled) / (1 + np.exp(logits_temp_scaled))

# Evaluate Brier score
brier_temp_scaled = brier_score_loss(y_test, y_pred_probs_temp_scaled)
print(f"Brier score after Temperature Scaling: {brier_temp_scaled:.4f}")
Brier score after Temperature Scaling: 0.1087

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

Байесовское объединение в квантили (BBQ)

Байесовское объединение в квантили (BBQ) и другие методы напрямую не доступны в scikit-learn.

Однако вы можете реализовать его вручную. Вот пример того, как применить калибровку BBQ к тому же синтетическому набору данных и базовому классификатору SVM из предыдущих примеров:

import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.metrics import brier_score_loss
from scipy.stats import beta

# Create synthetic dataset
X, y = make_classification(n_samples=1000, n_features=20, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Train an SVM classifier
base_classifier = SVC(kernel='linear', C=1, probability=True)
base_classifier.fit(X_train, y_train)

# Get predicted probabilities
y_pred_probs = base_classifier.predict_proba(X_test)[:, 1]

def bbq_calibration(y_true, y_pred_probs, n_bins=10):
    bin_edges = np.linspace(0, 1, n_bins + 1)
    bin_indices = np.digitize(y_pred_probs, bin_edges) - 1
    bin_true_probs = np.zeros(n_bins)
    
    for i in range(n_bins):
        mask = bin_indices == i
        bin_true_probs[i] = np.mean(y_true[mask]) if np.sum(mask) > 0 else 0.5

    return bin_true_probs[bin_indices]

# Calibrate using Bayesian Binning into Quantiles (BBQ)
y_pred_probs_bbq = bbq_calibration(y_test, y_pred_probs)

# Evaluate Brier score
brier_bbq = brier_score_loss(y_test, y_pred_probs_bbq)
print(f"Brier score after Bayesian Binning into Quantiles (BBQ): {brier_bbq:.4f}")
Brier score after Bayesian Binning into Quantiles (BBQ): 0.1009

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

Калибровка с перекрестной проверкой:

# Calibrate using cross-validated Platt Scaling
platt_calibrator_cv = CalibratedClassifierCV(base_classifier, method='sigmoid', cv=5)
platt_calibrator_cv.fit(X_train, y_train)

# Get cross-validated Platt calibrated probabilities
y_pred_probs_platt_cv = platt_calibrator_cv.predict_proba(X_test)[:, 1]

# Evaluate Brier score
brier_platt_cv = brier_score_loss(y_test, y_pred_probs_platt_cv)
print(f"Brier score after cross-validated Platt Scaling: {brier_platt_cv:.4f}")
Brier score after cross-validated Platt Scaling: 0.1094

Калибровка ансамбля:

from sklearn.ensemble import RandomForestClassifier

# Train a RandomForest classifier
ensemble_classifier = RandomForestClassifier(n_estimators=100, random_state=42)
ensemble_classifier.fit(X_train, y_train)

# Get predicted probabilities
y_pred_probs_ensemble = ensemble_classifier.predict_proba(X_test)[:, 1]

# Evaluate Brier score
brier_ensemble = brier_score_loss(y_test, y_pred_probs_ensemble)
print(f"Brier score of ensemble classifier: {brier_ensemble:.4f}")
Brier score of ensemble classifier: 0.0936

Обучение с учетом затрат:

from sklearn.svm import SVC

# Define a custom cost matrix
cost_matrix = np.array([[0, 1.5],  # [0, 1] cost
                        [1, 0]])   # [1, 0] cost

# Train a cost-sensitive SVM classifier
cs_svm = SVC(kernel='linear', C=1, class_weight={0: cost_matrix[0, 1], 1: cost_matrix[1, 0]})
cs_svm.fit(X_train, y_train)

# Get predicted probabilities
y_pred_probs_cs = cs_svm.predict_proba(X_test)[:, 1]

# Evaluate Brier score
brier_cs = brier_score_loss(y_test, y_pred_probs_cs)
print(f"Brier score of cost-sensitive classifier: {brier_cs:.4f}")
Brier score of cost-sensitive classifier: 0.1105

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

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

import plotly.graph_objects as go

# Create a scatter plot with an ideal calibration curve
fig = go.Figure(go.Scatter(x=[0, 1], y=[0, 1], 
                           mode='lines', 
                           line=dict(dash='dash', color='gray'), 
                           name='Ideal Calibration'))

colors_list = ['#1f77b4',  # muted blue
               '#ff7f0e',  # safety orange
               '#d62728',  # brick red
               '#9467bd',  # muted purple
               '#8c564b',  # chestnut brown
               '#e377c2',  # raspberry yogurt pink
               '#7f7f7f',  # middle gray
               '#bcbd22',  # curry yellow-green
               '#17becf'  # blue-teal
                ]
# Add the calibration curve for each method as invisible traces
methods = [('Platt Scaling', y_pred_probs_platt),
           ('Isotonic Regression', y_pred_probs_isotonic),
           ('Temperature Scaling', y_pred_probs_temp_scaled),
           ('BBQ', y_pred_probs_bbq),
           ('Cross-validated Platt Scaling', y_pred_probs_platt_cv),
           ('Ensemble Calibration', y_pred_probs_ensemble),
           ('Cost-sensitive Learning', y_pred_probs_cs)
           ]
L = len(methods)

#for method, y_pred_probs_calibrated in methods:


# Define the dropdown menu
dropdown_menu = [
    {
        "args": [{"visible": [False] * L, "showlegend": [False] * L}],
        "label": "Select Method",
        "method": "update",
    }
]

for idx, (method, y_pred_probs_calibrated) in enumerate(methods):
    fig.add_trace(go.Scatter(x=y_pred_probs, y=y_pred_probs_calibrated, 
                             mode='markers', 
                             marker=dict(size=6, color=colors_list[idx],opacity=0.5),
                             name=method, 
                             visible=False))
    visible_list = [False] * L
    visible_list[(idx+1)%L] = True
    showlegend_list = [False] * L
    showlegend_list[(idx+1)%L] = True
    dropdown_menu.append(
        {
            "args": [
                {
                    "visible": visible_list,
                    "showlegend": showlegend_list,
                    "annotations": [
                        {
                            "text": method,
                            "showarrow": False,
                            "xref": "paper",
                            "yref": "paper",
                            "x": 1,
                            "y": 1,
                            "xanchor": "right",
                            "yanchor": "bottom",
                            "font": {"size": 16},
                        },
                        {
                            "text": "Ideal Calibration",
                            "showarrow": False,
                            "xref": "paper",
                            "yref": "paper",
                            "x": 1,
                            "y": 1,
                            "xanchor": "right",
                            "yanchor": "bottom",
                            "font":{"size": 16},
                        }
                    ],
                }
            ],
            "label": method,
            "method": "update",
        }
    )

fig.update_layout(
    updatemenus=[
        go.layout.Updatemenu(
            buttons=dropdown_menu,
            showactive=False,
            direction="down",
            pad={"r": 10, "t": 10},
            x=0.05,
            xanchor="left",
            y=1.15,
            yanchor="top",
        )
    ]
)

# Add axis labels
fig.update_xaxes(title_text="Predicted Probabilities (Uncalibrated)")
fig.update_yaxes(title_text="Predicted Probabilities (Calibrated)")

# Show the interactive plot
fig.show()

Дайте мне знать, что вы думаете!

👏 Не забудьте поставить лайк этой статье и поделиться ею со своей сетью, чтобы поддержать мою работу! Не стесняйтесь подписываться на мой профиль на Medium, чтобы получать больше информации о машинном обучении и науке о данных. Спасибо за такую ​​поддержку! 🚀