Prédiction conforme pour la classification d’apprentissage automatique – De A à Z

This title emphasizes the accuracy of the prediction and presents a comprehensive approach to machine learning classification, covering everything from the beginning to the end.

Implémentation de la prédiction conforme pour la classification sans besoin de packages sur mesure

Cet article de blog est inspiré du livre de Chris Molner – Introduction à la prédiction conforme avec Python. Chris est brillant pour rendre les nouvelles techniques d’apprentissage automatique accessibles à tous. Je recommande également vivement ses livres sur l’apprentissage automatique explicatif.

Un dépôt GitHub avec le code complet peut être trouvé ici: Prédiction conforme.

Qu’est-ce que la prédiction conforme ?

La prédiction conforme est à la fois une méthode de quantification de l’incertitude et une méthode de classification des instances (qui peut être adaptée aux classes ou aux sous-groupes). L’incertitude est transmise par la classification en ensembles de classes potentielles plutôt qu’en prédictions uniques.

La prédiction conforme spécifie une couverture, qui indique la probabilité que le résultat réel soit couvert par la région de prédiction. L’interprétation des régions de prédiction dans la prédiction conforme dépend de la tâche. Pour la classification, nous obtenons des ensembles de prédictions, tandis que pour la régression, nous obtenons des intervalles de prédictions.

Voici un exemple de la différence entre la classification “traditionnelle” (équilibre de vraisemblance) et la prédiction conforme (ensembles) :

La différence entre la classification 'normale' basée sur la classe la plus probable et la prédiction conforme qui crée des ensembles de classes possibles.

Les avantages de cette méthode sont les suivants :

Couverture garantie : Les ensembles de prédictions générés par la prédiction conforme sont garantis d’inclure le résultat réel, c’est-à-dire qu’ils détecteront le pourcentage de valeurs réelles que vous avez défini comme cible minimale. La prédiction conforme ne dépend pas d’un modèle bien calibré – la seule chose qui compte, comme pour tout apprentissage automatique, c’est que les nouveaux échantillons classifiés proviennent de distributions de données similaires aux données d’entraînement et d’étalonnage. La couverture peut également être garantie pour les classes ou les sous-groupes, bien que cela nécessite une étape supplémentaire dans la méthode que nous aborderons.

  • Facile à utiliser : Les approches de prédiction conforme peuvent être mises en œuvre à partir de zéro avec seulement quelques lignes de code, comme nous le ferons ici.
  • Agnostique du modèle : La prédiction conforme fonctionne avec n’importe quel modèle d’apprentissage automatique. Elle utilise les sorties normales de votre modèle préféré.
  • Indépendant de la distribution : La prédiction conforme ne fait aucune hypothèse sur les distributions sous-jacentes des données ; c’est une méthode non paramétrique.
  • Aucune nouvelle formation requise : La prédiction conforme peut être utilisée sans reformation de votre modèle. C’est une autre façon d’analyser et d’utiliser les sorties du modèle.
  • Large application : La prédiction conforme fonctionne pour la classification de données tabulaires, la classification d’images ou de séries chronologiques, la régression et de nombreuses autres tâches, bien que nous ne démontrerons ici que la classification.

Pourquoi devrions-nous nous soucier de la quantification de l’incertitude ?

La quantification de l’incertitude est essentielle dans de nombreuses situations :

  • Lorsque nous utilisons des prédictions de modèle pour prendre des décisions. À quel point sommes-nous sûrs de ces prédictions ? Est-il suffisant d’utiliser simplement la “classe la plus probable” pour la tâche que nous avons ?
  • Lorsque nous voulons communiquer l’incertitude associée à nos prédictions aux parties prenantes, sans parler de probabilités ni de cotes, voire même de logarithmes de cotes !

Alpha dans la prédiction conforme – décrit la couverture

La couverture est fondamentale dans la prédiction conforme. En classification, il s’agit de la région normale des données qu’une classe particulière habite. La couverture est équivalente à la sensibilité ou au rappel ; il s’agit de la proportion de valeurs observées qui sont identifiées dans les ensembles de classification. Nous pouvons resserrer ou relâcher la zone de couverture en ajustant 𝛼 (couverture = 1 – 𝛼).

Codons !

Importer les packages

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.datasets import make_blobs
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

Créer des données synthétiques pour la classification

Des données d’exemple seront produites en utilisant la méthode `make_blobs` de SK-Learn.

n_classes = 3
# Créer des données d'entraînement et de test
X, y = make_blobs(n_samples=10000, n_features=2, centers=n_classes, cluster_std=3.75, random_state=42)
# Réduire la taille de la première classe pour créer un ensemble de données déséquilibré
# Définir la graine aléatoire numpy
np.random.seed(42)
# Obtenir l'indice lorsque y est de classe 0
class_0_idx = np.where(y == 0)[0]
# Obtenir 30 % des indices de la classe 0
class_0_idx = np.random.choice(class_0_idx, int(len(class_0_idx) * 0.3), replace=False)
# Obtenir l'indice pour toutes les autres classes
rest_idx = np.where(y != 0)[0]
# Combiner les indices
idx = np.concatenate([class_0_idx, rest_idx])
# Mélanger les indices
np.random.shuffle(idx)
# Diviser les données
X = X[idx]
y = y[idx]
# Diviser le jeu de données d'entraînement
X_train, X_rest, y_train, y_rest = train_test_split(X, y, test_size=0.5, random_state=42)
# Diviser le reste en ensemble d'étalonnage et de test
X_Cal, X_test, y_cal, y_test = train_test_split(X_rest, y_rest, test_size=0.5, random_state=42)
# Définir les étiquettes de classe
class_labels = ['bleu', 'orange', 'vert']

# Tracer les données
fig = plt.subplots(figsize=(5, 5))
ax = plt.subplot(111)
for i in range(n_classes):
    ax.scatter(X_test[y_test == i, 0], X_test[y_test == i, 1], label=class_labels[i], alpha=0.5, s=10)
legend = ax.legend()
legend.set_title("Classe")
ax.set_xlabel("Caractéristique 1")
ax.set_ylabel("Caractéristique 2")
plt.show()
Données générées (les données sont créées de manière déséquilibrée - la classe bleue ne représente qu'environ 30 % des points de données des classes verte ou orange).

Construire un classifieur

Nous utiliserons un modèle de régression logistique simple ici, mais la méthode peut fonctionner avec n’importe quel modèle, depuis un modèle de régression logistique simple basé sur des données tabulaires jusqu’à des ConvNets 3D pour la classification d’images.

# Construire et entraîner le classifieur
classifieur = LogisticRegression(random_state=42)
classifieur.fit(X_train, y_train)
# Tester le classifieur
y_pred = classifieur.predict(X_test)
exactitude = np.mean(y_pred == y_test)
print(f"Exactitude : {exactitude:0.3f}")
# Tester le rappel pour chaque classe
for i in range(n_classes):
    rappel = np.mean(y_pred[y_test == i] == y_test[y_test == i])
    print(f"Rappel pour la classe {class_labels[i]} : {rappel:0.3f}")

Exactitude : 0.930
Rappel pour la classe bleu : 0.772
Rappel pour la classe orange : 0.938
Rappel pour la classe vert : 0.969

Remarquez comment le rappel pour la classe minoritaire est plus bas que pour les autres classes. Le rappel, également connu sous le nom de sensibilité, est le nombre dans une classe qui est correctement identifié par le classifieur.

S_i, ou le score de non-conformité scores

Dans la prédiction conforme, le score de non-conformité, souvent noté s_i, est une mesure de l’écart d’une nouvelle instance par rapport aux instances existantes dans l’ensemble d’entraînement. Il est utilisé pour déterminer si une nouvelle instance appartient à une classe particulière ou non.

Dans le contexte de la classification, la mesure de non-conformité la plus courante est 1 – la probabilité de classe prédite pour l’étiquette donnée. Ainsi, si la probabilité prédite qu’une nouvelle instance appartienne à une certaine classe est élevée, le score de non-conformité sera faible, et vice versa.

Pour la prédiction conforme, nous obtenons des scores s_i pour toutes les classes (note : nous ne considérons que la sortie du modèle pour la vraie classe d’une instance, même si elle a une probabilité prédite plus élevée d’appartenir à une autre classe). Nous trouvons ensuite un seuil de scores qui contient (ou couvre) 95 % des données. La classification permettra alors d’identifier 95 % des nouvelles instances (tant que nos nouvelles données sont similaires à nos données d’entraînement).

Calculer le seuil de prédiction conforme

Nous allons maintenant prédire les probabilités de classification de l’ensemble d’étalonnage. Cela sera utilisé pour fixer un seuil de classification pour les nouvelles données.

# Obtenir les prédictions pour l'ensemble d'étalonnagey_pred = classifier.predict(X_Cal)y_pred_proba = classifier.predict_proba(X_Cal)# Afficher les 5 premières instancesy_pred_proba[0:5]

array([[4.65677826e-04, 1.29602253e-03, 9.98238300e-01],       [1.73428257e-03, 1.20718182e-02, 9.86193899e-01],       [2.51649788e-01, 7.48331668e-01, 1.85434981e-05],       [5.97545130e-04, 3.51642214e-04, 9.99050813e-01],       [4.54193815e-06, 9.99983628e-01, 1.18300819e-05]])

Calculer les scores de non-conformité :

Nous calculerons les scores s_i uniquement en fonction des probabilités associées à la classe observée. Pour chaque instance, nous obtiendrons la probabilité prédite pour la classe de cette instance. Le score s_i (non-conformité) est égal à 1 moins la probabilité. Plus le score s_i est élevé, moins cet exemple se conforme à cette classe par rapport aux autres classes.

si_scores = []# Parcourir toutes les instances d'étalonnagefor i, true_class in enumerate(y_cal):    # Obtenir la probabilité prédite pour la classe observée/true    predicted_prob = y_pred_proba[i][true_class]    si_scores.append(1 - predicted_prob)    # Convertir en tableau NumPysi_scores = np.array(si_scores)# Afficher les 5 premières instancessi_scores[0:5]

array([1.76170035e-03, 1.38061008e-02, 2.51668332e-01, 9.49187344e-04,       1.63720201e-05])

Obtenir le seuil du 95e percentile :

Le seuil détermine quelle couverture aura notre classification. La couverture fait référence à la proportion de prédictions qui contiennent réellement le résultat réel.

Le seuil est le percentile correspondant à 1 – 𝛼. Pour avoir une couverture de 95 %, nous fixons un 𝛼 de 0,05.

Lorsqu’il est utilisé dans la vie réelle, le niveau de quantile (basé sur 𝛼) nécessite une correction d’échantillonnage finie pour calculer le quantile correspondant 𝑞. Nous multiplions 0,95 par $(n+ 1)/n$, ce qui signifie que 𝑞𝑙𝑒𝑣𝑒𝑙 serait de 0,951 pour n = 1000.

nombre_d'échantillons = len(X_Cal)alpha = 0,05niveau_q = (1 - alpha) * ((nombre_d'échantillons + 1) / nombre_d'échantillons)seuil = np.percentile(si_scores, niveau_q*100)print(f'Seuil : {seuil:0.3f}')

Seuil : 0.598

Afficher le graphique des valeurs s_i, avec le seuil de coupure.

x = np.arange(len(si_scores)) + 1sorted_si_scores = np.sort(si_scores)index_du_95e_percentile = int(len(si_scores) * 0,95)# Colorier selon le seuilconforme = 'g' * index_du_95e_percentilenonconforme = 'r' * (len(si_scores) - index_du_95e_percentile)couleur = liste(conforme + nonconforme)fig = plt.figure(figsize=((6,4)))ax = fig.add_subplot()# Ajouter des barresax.bar(x, sorted_si_scores, width=1.0, color = couleur)# Ajouter des lignes pour le 95e percentileax.plot([0, index_du_95e_percentile],[seuil, seuil],         c='k', linestyle='--')ax.plot([index_du_95e_percentile, index_du_95e_percentile], [seuil, 0],        c='k', linestyle='--')# Ajouter du textetxt = 'Seuil de conformité du 95e percentile'ax.text(5, seuil + 0,04, txt)# Ajouter des étiquettes d'axeax.set_xlabel('Instance d'échantillon (triée par $s_i$)')ax.set_ylabel('$S_i$ (non-conformité)')plt.show()
s_i scores for all data. The threshold is the s_i level that contains 95% of all data (if 𝛼 is set at 0.05).

Obtenir les échantillons/classes de l’ensemble de test classés comme positifs

Nous pouvons maintenant trouver toutes les sorties du modèle inférieures au seuil.

Il est possible qu’un exemple individuel n’ait aucune valeur ou plus d’une valeur en dessous du seuil.

prediction_sets = (1 - classifier.predict_proba(X_test) <= seuil)# Afficher les dix premières instancesprediction_sets[0:10]

array([[ True, False, False],       [False, False,  True],       [ True, False, False],       [False, False,  True],       [False,  True, False],       [False,  True, False],       [False,  True, False],       [ True,  True, False],       [False,  True, False],       [False,  True, False]])

Obtenir les étiquettes de l’ensemble de prédictions et les comparer à la classification standard.

# Obtenir les prédictions standardy_pred = classifier.predict(X_test)# Fonction pour obtenir les étiquettes de l'ensemble de prédictionsdef get_prediction_set_labels(prediction_set, class_labels):    # Obtenir l'ensemble des étiquettes de classe pour chaque instance dans les ensembles de prédictions    prediction_set_labels = [        set([class_labels[i] for i, x in enumerate(prediction_set) if x]) for prediction_set in         prediction_sets]    return prediction_set_labels# Regrouper les prédictionsresults_sets = pd.DataFrame()results_sets['observed'] = [class_labels[i] for i in y_test]results_sets['labels'] = get_prediction_set_labels(prediction_sets, class_labels)results_sets['classifications'] = [class_labels[i] for i in y_pred]results_sets.head(10)

   observed  labels           classifications0  bleu      {bleu}           bleu1  vert      {vert}          vert2  bleu      {bleu}           bleu3  vert     {vert}           vert4  orange    {orange}         orange5  orange    {orange}         orange6  orange    {orange}         orange7  orange    {bleu, orange}   bleu8  orange    {orange}         orange9  orange    {orange}         orange

Notez que l’instance 7 est réellement de la classe orange, mais a été classée par le classificateur simple comme bleue. La prédiction conforme la considère comme un ensemble d’orange et de bleu.

Tracer les données montrant l’instance 7 qui est prédite comme pouvant appartenir à 2 classes :

# Tracer les donnéesfig = plt.subplots(figsize=(5, 5))ax = plt.subplot(111)for i in range(n_classes):    ax.scatter(X_test[y_test == i, 0], X_test[y_test == i, 1],               label=class_labels[i], alpha=0.5, s=10)# Ajouter l'instance 7set_label = results_sets['labels'].iloc[7]ax.scatter(X_test[7, 0], X_test[7, 1], color='k', s=100, marker='*', label=f'Instance 7')legend = ax.legend()legend.set_title("Classe")ax.set_xlabel("Caractéristique 1")ax.set_ylabel("Caractéristique 2")txt = f"Ensemble de prédictions pour l'instance 7 : {set_label}"ax.text(-20, 18, txt)plt.show()
Diagramme de dispersion montrant comment l'instance 7 de test a été classée comme appartenant à deux ensembles possibles : {‘bleu’, ‘orange’},

Afficher la couverture et la taille moyenne de l’ensemble

La couverture est la proportion des ensembles de prédictions qui contiennent réellement le résultat exact.

La taille moyenne de l’ensemble est le nombre moyen de classes prédites par instance.

Nous allons définir quelques fonctions pour calculer les résultats.

# Obtenir le nombre de classesdef get_class_counts(y_test):    class_counts = []    for i in range(n_classes):        class_counts.append(np.sum(y_test == i))    return class_counts# Obtenir la couverture pour chaque classedef get_coverage_by_class(prediction_sets, y_test):    coverage = []    for i in range(n_classes):        coverage.append(np.mean(prediction_sets[y_test == i, i]))    return coverage# Obtenir la taille moyenne de l'ensemble pour chaque classedef get_average_set_size(prediction_sets, y_test):    average_set_size = []    for i in range(n_classes):        average_set_size.append(            np.mean(np.sum(prediction_sets[y_test == i], axis=1)))    return average_set_size     # Obtenir la couverture pondérée (pondérée par la taille de la classe)def get_weighted_coverage(coverage, class_counts):    total_counts = np.sum(class_counts)    weighted_coverage = np.sum((coverage * class_counts) / total_counts)    weighted_coverage = round(weighted_coverage, 3)    return weighted_coverage# Obtenir la taille pondérée de l'ensemble (pondérée par la taille de la classe)def get_weighted_set_size(set_size, class_counts):    total_counts = np.sum(class_counts)    weighted_set_size = np.sum((set_size * class_counts) / total_counts)    weighted_set_size = round(weighted_set_size, 3)    return weighted_set_size

Afficher les résultats pour chaque classe.

results = pd.DataFrame(index=class_labels)results['Nombre de classes'] = get_class_counts(y_test)results['Couverture'] = get_coverage_by_class(prediction_sets, y_test)results['Taille moyenne de l'ensemble'] = get_average_set_size(prediction_sets, y_test)results

        Nombre de classes Couverture  Taille moyenne de l'ensembleblue    241                 0.817427   1.087137orange  848                 0.954009   1.037736green   828                 0.977053   1.016908

Afficher les résultats globaux.

weighted_coverage = get_weighted_coverage(    results['Couverture'], results['Nombre de classes'])weighted_set_size = get_weighted_set_size(    results['Taille moyenne de l'ensemble'], results['Nombre de classes'])print (f'Couverture globale : {weighted_coverage}')print (f'Taille moyenne de l'ensemble : {weighted_set_size}')

Couverture globale : 0.947Taille moyenne de l'ensemble : 1.035

NOTE : Bien que notre couverture globale soit conforme à nos attentes, avec une valeur très proche de 95 %, la couverture des différentes classes varie et est la plus faible (83 %) pour notre plus petite classe. Si la couverture des classes individuelles est importante, nous pouvons définir des seuils pour les classes de manière indépendante, ce que nous allons faire maintenant.

Classification conforme avec une couverture égale pour toutes les classes

Lorsque nous voulons être sûrs d’avoir une couverture pour toutes les classes, nous pouvons définir des seuils pour chaque classe indépendamment.

Remarque : nous pourrions également le faire pour des sous-groupes de données, tels que garantir une couverture égale pour un diagnostic dans des groupes raciaux, si nous constations que la couverture en utilisant un seuil commun posait des problèmes.

Obtenir des seuils pour chaque classe de manière indépendante

# Définir alpha (1 - couverture)alpha = 0.05thresholds = []# Obtenir les probabilités prédites pour l'ensemble d'étalonnagey_cal_prob = classifier.predict_proba(X_Cal)# Obtenir le score du 95e percentile pour les s-scores de chaque classepour class_label in range(n_classes):    masque = y_cal == class_label    y_cal_prob_class = y_cal_prob[masque][:, class_label]    s_scores = 1 - y_cal_prob_class    q = (1 - alpha) * 100    taille_classe = masque.sum()    correction = (taille_classe + 1) / taille_classe    q *= correction    seuil = np.percentile(s_scores, q)    thresholds.append(threshold)print(thresholds)

[0.9030202125697161, 0.6317149025299887, 0.26033562285411]

Appliquer un seuil spécifique à chaque classe pour la classification de chaque classe

# Obtenir les scores Si pour l'ensemble de testpredicted_proba = classifier.predict_proba(X_test)si_scores = 1 - predicted_proba# Pour chaque classe, vérifier si chaque instance est inférieure au seuilprediction_sets = []pour i in range(n_classes):    prediction_sets.append(si_scores[:, i] <= thresholds[i])prediction_sets = np.array(prediction_sets).T# Obtenir les étiquettes de l'ensemble de prédictions et afficher les 10 premièresétiquettes_ensemble_prediction = get_prediction_set_labels(prediction_sets, class_labels)# Obtenir les prédictions standardy_pred = classifier.predict(X_test)# Colliger les prédictionsresults_sets = pd.DataFrame()results_sets['observé'] = [class_labels[i] pour i in y_test]results_sets['étiquettes'] = get_prediction_set_labels(prediction_sets, class_labels)results_sets['classifications'] = [class_labels[i] pour i in y_pred]# Afficher les 10 premiers résultatsresults_sets.head(10)

  observé  étiquettes       classifications0 blue     {blue}        blue1 green    {green}       green2 blue     {blue}        blue3 green    {green}       green4 orange   {orange}      orange5 orange   {orange}      orange6 orange   {orange}      orange7 orange   {blue, orange}  blue8 orange   {orange}      orange9 orange   {orange}      orange

Vérifiez la couverture et définissez la taille à travers les classes

Nous avons maintenant une couverture d’environ 95% pour l’ensemble des classes. La méthode de prédiction conforme nous donne une meilleure couverture de la classe minoritaire que la méthode standard de classification.

results = pd.DataFrame(index=class_labels)results['Nombre de classes'] = get_class_counts(y_test)results['Couverture'] = get_coverage_by_class(prediction_sets, y_test)results['Taille moyenne des ensembles'] = get_average_set_size(prediction_sets, y_test)results

        Nombre de classes  Couverture  Taille moyenne des ensemblesbleue    241                0.954357    1.228216orange  848                0.956368    1.139151verte    828                0.942029    1.006039

couverture_pondérée = get_weighted_coverage(    results['Couverture'], results['Nombre de classes'])taille_pondérée_des_ensembles = get_weighted_set_size(    results['Taille moyenne des ensembles'], results['Nombre de classes'])print (f'Couverture globale : {couverture_pondérée}')print (f'Taille moyenne des ensembles : {taille_pondérée_des_ensembles}')

Couverture globale : 0.95Taille moyenne des ensembles : 1.093

Résumé

La prédiction conforme a été utilisée pour classifier des instances en ensembles plutôt que des prédictions individuelles. Les instances situées aux frontières entre deux classes ont été étiquetées avec les deux classes plutôt que de choisir la classe avec la probabilité la plus élevée.

Lorsqu’il est important de détecter toutes les classes avec la même couverture, le seuil de classification des instances peut être défini séparément (cette méthode pourrait également être utilisée pour des sous-groupes de données, par exemple pour garantir la même couverture entre différents groupes ethniques).

La prédiction conforme ne modifie pas les prédictions du modèle. Elle les utilise simplement d’une manière différente de la classification traditionnelle. Elle peut être utilisée en complément de méthodes plus traditionnelles.

(Toutes les images sont de l’auteur)

We will continue to update IPGirl; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Science des données

L'IA et les jumeaux numériques pour libérer la prochaine vague d'innovation dans la recherche sur le climat

L’intelligence artificielle et le calcul accéléré aideront les chercheurs en climatologie à réaliser les miracl...

AI

Pas le Vader que vous pensez 3D VADER est un modèle d'IA qui diffuse des modèles 3D

La génération d’images n’a jamais été aussi facile. Avec la montée en puissance des modèles d’IA gé...

AI

Décryptage de la régulation génique avec l'apprentissage profond Une nouvelle approche de l'IA pour comprendre l'épissage alternatif

L’épissage alternatif est un processus fondamental dans la régulation des gènes, permettant à un seul gène de p...

Actualités sur l'IA

Définissez des autorisations personnalisées en quelques minutes avec Amazon SageMaker Role Manager via l'AWS CDK.

Les administrateurs de l'apprentissage automatique (ML) jouent un rôle crucial dans le maintien de la sécurité et de ...

AI

Comment j'ai créé une animation des embeddings pendant le fine-tuning

Dans le domaine de l'apprentissage automatique, les Vision Transformers (ViT) sont un type de modèle utilisé pour la ...