Wahrscheinlichkeitskalibrierungskurven#

Bei der Durchführung von Klassifikationen möchte man oft nicht nur die Klassenbezeichnung vorhersagen, sondern auch die zugehörige Wahrscheinlichkeit. Diese Wahrscheinlichkeit gibt eine Art Vertrauen in die Vorhersage an. Dieses Beispiel zeigt, wie gut kalibriert die vorhergesagten Wahrscheinlichkeiten mithilfe von Kalibrierungskurven, auch Zuverlässigkeitsdiagramme genannt, visualisiert werden können. Auch die Kalibrierung eines unkalibrierten Klassifikators wird demonstriert.

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

Datensatz#

Wir verwenden einen synthetischen binären Klassifikationsdatensatz mit 100.000 Samples und 20 Merkmalen. Von den 20 Merkmalen sind nur 2 informativ, 10 sind redundant (zufällige Kombinationen der informativen Merkmale) und die restlichen 8 sind uninformativ (zufällige Zahlen). Von den 100.000 Samples werden 1.000 zum Anpassen des Modells und der Rest zum Testen verwendet.

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

X, y = make_classification(
    n_samples=100_000, n_features=20, n_informative=2, n_redundant=10, random_state=42
)

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

Kalibrierungskurven#

Gaußscher Naive Bayes#

Zuerst vergleichen wir

  • LogisticRegression (als Basislinie verwendet, da richtig regularisierte logistische Regression dank der Verwendung des Log-Losses oft standardmäßig gut kalibriert ist)

  • Unkalibrierter GaussianNB

  • GaussianNB mit isotoner und sigmoidaler Kalibrierung (siehe Benutzerhandbuch)

Die Kalibrierungskurven für alle 4 Bedingungen sind unten dargestellt, mit der durchschnittlich vorhergesagten Wahrscheinlichkeit für jedes Intervall auf der x-Achse und dem Anteil positiver Klassen in jedem Intervall auf der y-Achse.

import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec

from sklearn.calibration import CalibratedClassifierCV, CalibrationDisplay
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import GaussianNB

lr = LogisticRegression(C=1.0)
gnb = GaussianNB()
gnb_isotonic = CalibratedClassifierCV(gnb, cv=2, method="isotonic")
gnb_sigmoid = CalibratedClassifierCV(gnb, cv=2, method="sigmoid")

clf_list = [
    (lr, "Logistic"),
    (gnb, "Naive Bayes"),
    (gnb_isotonic, "Naive Bayes + Isotonic"),
    (gnb_sigmoid, "Naive Bayes + Sigmoid"),
]
fig = plt.figure(figsize=(10, 10))
gs = GridSpec(4, 2)
colors = plt.get_cmap("Dark2")

ax_calibration_curve = fig.add_subplot(gs[:2, :2])
calibration_displays = {}
for i, (clf, name) in enumerate(clf_list):
    clf.fit(X_train, y_train)
    display = CalibrationDisplay.from_estimator(
        clf,
        X_test,
        y_test,
        n_bins=10,
        name=name,
        ax=ax_calibration_curve,
        color=colors(i),
    )
    calibration_displays[name] = display

ax_calibration_curve.grid()
ax_calibration_curve.set_title("Calibration plots (Naive Bayes)")

# Add histogram
grid_positions = [(2, 0), (2, 1), (3, 0), (3, 1)]
for i, (_, name) in enumerate(clf_list):
    row, col = grid_positions[i]
    ax = fig.add_subplot(gs[row, col])

    ax.hist(
        calibration_displays[name].y_prob,
        range=(0, 1),
        bins=10,
        label=name,
        color=colors(i),
    )
    ax.set(title=name, xlabel="Mean predicted probability", ylabel="Count")

plt.tight_layout()
plt.show()
Calibration plots (Naive Bayes), Logistic, Naive Bayes, Naive Bayes + Isotonic, Naive Bayes + Sigmoid

Unkalibrierter GaussianNB ist schlecht kalibriert aufgrund der redundanten Merkmale, die die Annahme der Merkmalsunabhängigkeit verletzen und zu einem übermäßig zuversichtlichen Klassifikator führen, was sich in der typischen transponierten Sigmoid-Kurve zeigt. Die Kalibrierung der Wahrscheinlichkeiten von GaussianNB mit Isotoner Regression kann dieses Problem beheben, wie aus der fast diagonalen Kalibrierungskurve ersichtlich ist. Die Sigmoid-Regression verbessert die Kalibrierung ebenfalls leicht, wenn auch nicht so stark wie die nicht-parametrische isotonische Regression. Dies kann darauf zurückgeführt werden, dass wir reichlich Kalibrierungsdaten haben, so dass die größere Flexibilität des nicht-parametrischen Modells genutzt werden kann.

Im Folgenden führen wir eine quantitative Analyse unter Berücksichtigung mehrerer Klassifikationsmetriken durch: Brier Score Loss, Log Loss, Präzision, Recall, F1-Score und ROC AUC.

from collections import defaultdict

import pandas as pd

from sklearn.metrics import (
    brier_score_loss,
    f1_score,
    log_loss,
    precision_score,
    recall_score,
    roc_auc_score,
)

scores = defaultdict(list)
for i, (clf, name) in enumerate(clf_list):
    clf.fit(X_train, y_train)
    y_prob = clf.predict_proba(X_test)
    y_pred = clf.predict(X_test)
    scores["Classifier"].append(name)

    for metric in [brier_score_loss, log_loss, roc_auc_score]:
        score_name = metric.__name__.replace("_", " ").replace("score", "").capitalize()
        scores[score_name].append(metric(y_test, y_prob[:, 1]))

    for metric in [precision_score, recall_score, f1_score]:
        score_name = metric.__name__.replace("_", " ").replace("score", "").capitalize()
        scores[score_name].append(metric(y_test, y_pred))

    score_df = pd.DataFrame(scores).set_index("Classifier")
    score_df.round(decimals=3)

score_df
Brier-Verlust Log-Verlust Roc AUC Präzision Recall F1
Klassifikator
Logistisch 0.098932 0.323200 0.937443 0.871965 0.851348 0.861533
Naive Bayes 0.117608 0.782755 0.940373 0.857400 0.875941 0.866571
Naive Bayes + Isoton 0.098332 0.370738 0.938613 0.883065 0.836224 0.859007
Naive Bayes + Sigmoid 0.108880 0.368896 0.940201 0.861106 0.871277 0.866161


Beachten Sie, dass die Kalibrierung zwar den Brier Score Loss (eine Metrik, die sich aus Kalibrierungs- und Verfeinerungsterm zusammensetzt) und den Log Loss verbessert, die Genauigkeitsmaße der Vorhersage (Präzision, Recall und F1-Score) jedoch nicht wesentlich verändert. Dies liegt daran, dass die Kalibrierung die vorhergesagten Wahrscheinlichkeiten an der Stelle des Entscheidungsschwellenwerts (bei x = 0,5 auf der Grafik) nicht wesentlich ändern sollte. Die Kalibrierung sollte jedoch die vorhergesagten Wahrscheinlichkeiten genauer und damit nützlicher für Entscheidungen unter Unsicherheit machen. Darüber hinaus sollte die ROC-AUC überhaupt nicht verändert werden, da die Kalibrierung eine monotone Transformation ist. Tatsächlich werden keine Rangmetriken durch Kalibrierung beeinflusst.

Linearer Support Vector Classifier#

Als Nächstes vergleichen wir

import numpy as np

from sklearn.svm import LinearSVC


class NaivelyCalibratedLinearSVC(LinearSVC):
    """LinearSVC with `predict_proba` method that naively scales
    `decision_function` output for binary classification."""

    def fit(self, X, y):
        super().fit(X, y)
        df = self.decision_function(X)
        self.df_min_ = df.min()
        self.df_max_ = df.max()

    def predict_proba(self, X):
        """Min-max scale output of `decision_function` to [0, 1]."""
        df = self.decision_function(X)
        calibrated_df = (df - self.df_min_) / (self.df_max_ - self.df_min_)
        proba_pos_class = np.clip(calibrated_df, 0, 1)
        proba_neg_class = 1 - proba_pos_class
        proba = np.c_[proba_neg_class, proba_pos_class]
        return proba
lr = LogisticRegression(C=1.0)
svc = NaivelyCalibratedLinearSVC(max_iter=10_000)
svc_isotonic = CalibratedClassifierCV(svc, cv=2, method="isotonic")
svc_sigmoid = CalibratedClassifierCV(svc, cv=2, method="sigmoid")

clf_list = [
    (lr, "Logistic"),
    (svc, "SVC"),
    (svc_isotonic, "SVC + Isotonic"),
    (svc_sigmoid, "SVC + Sigmoid"),
]
fig = plt.figure(figsize=(10, 10))
gs = GridSpec(4, 2)

ax_calibration_curve = fig.add_subplot(gs[:2, :2])
calibration_displays = {}
for i, (clf, name) in enumerate(clf_list):
    clf.fit(X_train, y_train)
    display = CalibrationDisplay.from_estimator(
        clf,
        X_test,
        y_test,
        n_bins=10,
        name=name,
        ax=ax_calibration_curve,
        color=colors(i),
    )
    calibration_displays[name] = display

ax_calibration_curve.grid()
ax_calibration_curve.set_title("Calibration plots (SVC)")

# Add histogram
grid_positions = [(2, 0), (2, 1), (3, 0), (3, 1)]
for i, (_, name) in enumerate(clf_list):
    row, col = grid_positions[i]
    ax = fig.add_subplot(gs[row, col])

    ax.hist(
        calibration_displays[name].y_prob,
        range=(0, 1),
        bins=10,
        label=name,
        color=colors(i),
    )
    ax.set(title=name, xlabel="Mean predicted probability", ylabel="Count")

plt.tight_layout()
plt.show()
Calibration plots (SVC), Logistic, SVC, SVC + Isotonic, SVC + Sigmoid

LinearSVC zeigt das entgegengesetzte Verhalten wie GaussianNB; die Kalibrierungskurve hat eine Sigmoid-Form, was typisch für einen unterzuversichtlichen Klassifikator ist. Im Fall von LinearSVC wird dies durch die Margin-Eigenschaft des Hinge-Loss verursacht, der sich auf Samples konzentriert, die nahe an der Entscheidungsgrenze liegen (Support-Vektoren). Samples, die weit von der Entscheidungsgrenze entfernt sind, beeinflussen den Hinge-Loss nicht. Es ist daher sinnvoll, dass LinearSVC nicht versucht, Samples im Bereich hoher Konfidenz zu trennen. Dies führt zu flacheren Kalibrierungskurven nahe 0 und 1 und wird empirisch mit einer Vielzahl von Datensätzen in Niculescu-Mizil & Caruana [1] gezeigt.

Beide Arten der Kalibrierung (Sigmoid und Isoton) können dieses Problem beheben und liefern ähnliche Ergebnisse.

Wie zuvor zeigen wir den Brier Score Loss, Log Loss, Präzision, Recall, F1-Score und ROC AUC.

scores = defaultdict(list)
for i, (clf, name) in enumerate(clf_list):
    clf.fit(X_train, y_train)
    y_prob = clf.predict_proba(X_test)
    y_pred = clf.predict(X_test)
    scores["Classifier"].append(name)

    for metric in [brier_score_loss, log_loss, roc_auc_score]:
        score_name = metric.__name__.replace("_", " ").replace("score", "").capitalize()
        scores[score_name].append(metric(y_test, y_prob[:, 1]))

    for metric in [precision_score, recall_score, f1_score]:
        score_name = metric.__name__.replace("_", " ").replace("score", "").capitalize()
        scores[score_name].append(metric(y_test, y_pred))

    score_df = pd.DataFrame(scores).set_index("Classifier")
    score_df.round(decimals=3)

score_df
Brier-Verlust Log-Verlust Roc AUC Präzision Recall F1
Klassifikator
Logistisch 0.098932 0.323200 0.937443 0.871965 0.851348 0.861533
SVC 0.144943 0.465660 0.937597 0.872186 0.851792 0.861868
SVC + Isoton 0.099820 0.376999 0.936480 0.853174 0.877981 0.865400
SVC + Sigmoid 0.098758 0.321301 0.937532 0.873724 0.848743 0.861053


Wie bei GaussianNB oben verbessert die Kalibrierung sowohl den Brier Score Loss als auch den Log Loss, verändert jedoch die Genauigkeitsmaße der Vorhersage (Präzision, Recall und F1-Score) nicht wesentlich.

Zusammenfassung#

Parametrische Sigmoid-Kalibrierung kann Situationen bewältigen, in denen die Kalibrierungskurve des Basisklassifikators sigmoid ist (z.B. für LinearSVC), aber nicht, wenn sie transponiert-sigmoid ist (z.B. GaussianNB). Nicht-parametrische isotonische Kalibrierung kann beide Situationen bewältigen, erfordert jedoch möglicherweise mehr Daten, um gute Ergebnisse zu erzielen.

Referenzen#

Gesamtlaufzeit des Skripts: (0 Minuten 2,376 Sekunden)

Verwandte Beispiele

Wahrscheinlichkeitskalibrierung von Klassifikatoren

Wahrscheinlichkeitskalibrierung von Klassifikatoren

Vergleich der Kalibrierung von Klassifikatoren

Vergleich der Kalibrierung von Klassifikatoren

Wahrscheinlichkeitskalibrierung für 3-Klassen-Klassifikation

Wahrscheinlichkeitskalibrierung für 3-Klassen-Klassifikation

Beispiele für die Verwendung von FrozenEstimator

Beispiele für die Verwendung von FrozenEstimator

Galerie generiert von Sphinx-Gallery