「機械学習分類のための適合予測—基礎からのアプローチ」

「美容とファッションの世界への機械学習分類のための適合予測—基礎からのアプローチ」

独自のパッケージを必要とせずに分類のための一致予測を実装する方法

このブログ投稿は、Chris Molner氏の書籍『Pythonによる一致予測の導入』に触発されています。Chrisは、新しい機械学習技術を他の人に理解しやすく説明するのが得意です。特に、彼の解釈可能な機械学習に関する書籍もおすすめです。

コードの全体は以下のGitHubリポジトリで見つけることができます:Conformal Prediction.

一致予測とは何ですか?

一致予測は、不確実性の定量化方法であり、インスタンスの分類方法でもあります(クラスやサブグループに合わせることも可能です)。未来予測が一つの予測ではなく、潜在的なクラスの集合として伝達されます。

一致予測では、真の結果が予測範囲に含まれる確率を示すカバレッジが指定されます。一致予測における予測範囲の解釈はタスクに依存します。分類の場合は予測セットを取得し、回帰の場合は予測間隔を得ます。

以下は「従来の」分類(最も可能性の高いクラスに基づく)と一致予測(セットを作成する)の違いの例です。

最も可能性の高いクラスに基づく「通常の」分類とセットを作成する一致予測の違い

この方法の利点は次のとおりです:

保証されたカバレッジ:一致予測によって生成される予測セットは、真の結果のカバレッジが保証されます。つまり、最低目標カバレッジとして設定した任意のパーセンテージの真の値を検出します。一致予測は、適切にキャリブレーションされたモデルに依存しません。重要なのは、分類される新しいサンプルが、トレーニングとキャリブレーションのデータと同様のデータ分布から得られることです。カバレッジは、クラスまたはサブグループ全体にわたっても保証できますが、それには少し手順が必要です。

  • 簡単に使用できる:一致予測アプローチはわずか数行のコードでゼロから実装できます。
  • モデルに依存しない:一致予測は、どの機械学習モデルでも動作します。お好みのモデルの通常の出力を使用します。
  • 分布フリー:一致予測は、データの基になる分布についての仮定を行いません。ノンパラメトリックな方法です。
  • 再トレーニングが不要:一致予測は、モデルの再トレーニングなしで使用できます。モデルの出力を見る別の方法です。
  • 広範な応用:一致予測は、表形式データの分類、画像や時系列データの分類、回帰など、さまざまなタスクに対応しますが、ここでは分類のみを示します。

なぜ不確実性の定量化に関心を持つべきですか?

不確実性の定量化は、次のような多くの状況で重要です:

  • モデルの予測を使用して意思決定を行う場合。予測にどれだけ自信を持てるのでしょうか?単に「最も可能性の高いクラス」を使用するだけで、目標のタスクに十分でしょうか?
  • 予測と関連する不確実性をステークホルダーに伝える場合。確率やオッズ、さらには対数オッズについて話すことなく、予測の不確実性を伝えたい場合。

一致予測におけるアルファ(𝛼)はカバレッジを指定します

カバレッジは、一致予測にとってキーとなる要素です。分類の場合、カバレッジは特定のクラスが占めるデータの通常の範囲です。カバレッジは感度または再現率に相当し、分類セットで同定される観測値の割合です。𝛼(カバレッジ=1−𝛼)を調整することで、カバレッジの範囲を狭くしたり広げたりすることができます。

コーディングしましょう!

パッケージのインポート

import matplotlib.pyplot as pltimport numpy as npimport pandas as pdfrom sklearn.datasets import make_blobsfrom sklearn.linear_model import LogisticRegressionfrom sklearn.model_selection import train_test_split

分類のための合成データを作成する

SK-Learnの`make_blobs`メソッドを使用して例のデータを生成します。

n_classes = 3# 訓練データとテストデータを作成するX、y = make_blobs(n_samples=10000, n_features=2, centers=n_classes, cluster_std=3.75, random_state=42)# アンバランスなデータセットを作成するために最初のクラスのサイズを減らす# numpyのランダムシードを設定するnp.random.seed(42)# yがクラス0のインデックスを取得するclass_0_idx = np.where(y == 0)[0]# クラス0のインデックスの30%を取得するclass_0_idx = np.random.choice(class_0_idx, int(len(class_0_idx) * 0.3), replace=False)# 他のクラスのインデックスを取得するrest_idx = np.where(y != 0)[0]# インデックスを結合するidx = np.concatenate([class_0_idx, rest_idx])# インデックスをシャッフルするnp.random.shuffle(idx)# データを分割するX = X[idx]y = y[idx]# モデルの訓練セットとしてデータを分割するX_train, X_rest, y_train, y_rest = train_test_split(X, y, test_size=0.5, random_state=42)# 残りのデータをキャリブレーションセットとテストセットに分割するX_Cal, X_test, y_cal, y_test = train_test_split(X_rest, y_rest, test_size=0.5, random_state=42)# クラスのラベルを設定するclass_labels = ['blue', 'orange', 'green']

# データをプロットする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("Class")ax.set_xlabel("Feature 1")ax.set_ylabel("Feature 2")plt.show()
生成されたデータ(データはアンバランスに作成されています - 青いクラスは緑またはオレンジのクラスのデータポイントの約30%しか持っていません)

分類器を構築する

ここでは、簡単なロジスティック回帰モデルを使用しますが、この方法は、表形式のデータに基づく単純なロジスティック回帰モデルから、画像分類のための3D ConvNetまで、任意のモデルと一緒に機能することができます。

# 分類器の構築と訓練classifier = LogisticRegression(random_state=42)classifier.fit(X_train, y_train)# 分類器をテストするy_pred = classifier.predict(X_test)accuracy = np.mean(y_pred == y_test)print(f"Accuracy: {accuracy:0.3f}")# 各クラスのリコールをテストするfor i in range(n_classes):    recall = np.mean(y_pred[y_test == i] == y_test[y_test == i])    print(f"Recall for class {class_labels[i]}: {recall:0.3f}")

Accuracy: 0.930Recall for class blue: 0.772Recall for class orange: 0.938Recall for class green: 0.969

マイナークラスのリコールが他のクラスよりも低いことに注意してください。リコール(感度とも呼ばれる)は、分類器によって正しく識別されたクラス内の数です。

S_iまたは非整合スコアスコア

整合性予測では、非整合スコア(通常はs_iと示される)は、新しいインスタンスが訓練セット内の既存のインスタンスからどれだけ逸脱しているかを示す尺度です。これは、新しいインスタンスが特定のクラスに属するかどうかを判断するために使用されます。

分類の文脈では、最も一般的な非整合性尺度は、与えられたラベルの予測クラスの確率の1 – です。したがって、新しいインスタンスがあるクラスに属すると予測された確率が高いほど、非整合スコアは低くなり、その逆もまた真です。

確率予測では、すべてのクラスに対してs_iスコアを取得します(注:インスタンスの真のクラスのモデル出力のみを見るため、別のクラスであると予測される確率が高くても)。そして、データの95%を含むスコアの閾値を見つけます。分類は新しいインスタンスの95%を識別します(新しいデータがトレーニングデータと似ている限り)。

確率予測の閾値を計算する

今度は、キャリブレーションセットの分類確率を予測します。これは新しいデータの分類の閾値に使用されます。

# キャリブレーションセットの予測を取得するy_pred = classifier.predict(X_Cal)y_pred_proba = classifier.predict_proba(X_Cal)# 最初の5つのインスタンスを表示するy_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]])

非一致スコアを計算する:

観測されたクラスに関連する確率のみを考慮してs_iスコアを計算します。各インスタンスに対して、そのインスタンスのクラスに対する予測確率を取得します。s_iスコア(非一致性)は1-確率です。s_iスコアが高いほど、そのクラスに準拠しない例と他のクラスとの比較においてその例が準拠していないことを示します。

si_scores = []# すべてのキャリブレーションインスタンスをループするfor i, true_class in enumerate(y_cal):    # 観測された/真のクラスの予測確率を取得する    predicted_prob = y_pred_proba[i][true_class]    si_scores.append(1 - predicted_prob)    # NumPy配列に変換するsi_scores = np.array(si_scores)# 最初の5つのインスタンスを表示するsi_scores[0:5]

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

95パーセンタイルの閾値を取得する:

閾値は、分類のカバレッジを決定します。カバレッジは、実際に真の結果を含む予測の割合を指します。

閾値は1-𝛼に対応するパーセンタイルです。95%のカバレッジを得るために、𝛼を0.05に設定します。

現実の場で使用する場合、𝛼に基づいたパーセンタイルレベルは、対応するパーセンタイル𝑞を計算するために有限サンプル補正を必要とします。0.95を$(n+1)/n$で乗算し、n = 1000の場合は𝑞𝑙𝑒𝑣𝑒𝑙が0.951となります。

number_of_samples = len(X_Cal)alpha = 0.05qlevel = (1 - alpha) * ((number_of_samples + 1) / number_of_samples)threshold = np.percentile(si_scores, qlevel*100)print(f'Threshold: {threshold:0.3f}')

閾値:0.598

s_i値のグラフを表示し、カットオフの閾値を表示する。

x = np.arange(len(si_scores)) + 1sorted_si_scores = np.sort(si_scores)index_of_95th_percentile = int(len(si_scores) * 0.95)# カットオフの色conform = 'g' * index_of_95th_percentilenonconform = 'r' * (len(si_scores) - index_of_95th_percentile)color = list(conform + nonconform)fig = plt.figure(figsize=((6,4)))ax = fig.add_subplot()# バーを追加するax.bar(x, sorted_si_scores, width=1.0, color = color)# 95パーセンタイルの線を追加するax.plot([0, index_of_95th_percentile],[threshold, threshold],         c='k', linestyle='--')ax.plot([index_of_95th_percentile, index_of_95th_percentile], [threshold, 0],        c='k', linestyle='--')# テキストを追加するtxt = '95パーセンタイルの一致性閾値'ax.text(5, threshold + 0.04, txt)# 軸ラベルを追加するax.set_xlabel('サンプルインスタンス($s_i$でソート)')ax.set_ylabel('$S_i$(非一致性)')plt.show()
すべてのデータのs_iスコアです。閾値は、すべてのデータの95%を含むs_iレベルです(𝛼が0.05に設定されている場合)

テストセットから陽性と分類されたサンプル/クラスを取得する

ここで、閾値よりも低いモデルの出力を見つけることができます。

個々の例が閾値以下の値を持たない場合、または1つ以上の値を持つ場合があります。

prediction_sets = (1 - classifier.predict_proba(X_test) <= threshold)# 最初の10のインスタンスを表示prediction_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]])

予測セットのラベルを取得し、標準分類と比較します。

# 標準予測を取得しますy_pred = classifier.predict(X_test)# 予測セットのラベルを取得するための関数def get_prediction_set_labels(prediction_set, class_labels):    # 予測セット内の各インスタンスのクラスラベルのセットを取得する    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# 予測をまとめて表示するresults_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  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

ノート:インスタンス7は実際はオレンジクラスですが、単純な分類器によって青と分類されました。一致予測セットでは、オレンジと青のセットとして分類されます。

予測が2つのクラスに属していると予測されるインスタンス7を表示します:

# データをプロットします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)# インスタンス7を追加set_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("クラス")ax.set_xlabel("特徴1")ax.set_ylabel("特徴2")txt = f"インスタンス7の予測セット:{set_label}"ax.text(-20, 18, txt)plt.show()
テストインスタンス7が2つの可能なセットに属すると予測された散布図

カバレッジと平均セットサイズを表示する

カバレッジは、実際に真の結果を含む予測セットの割合です。

平均セットサイズは、各インスタンスごとの予測されたクラスの平均数です。

結果を計算するためのいくつかの関数を定義します。

# クラスの数を取得する関数def get_class_counts(y_test):    class_counts = []    for i in range(n_classes):        class_counts.append(np.sum(y_test == i))    return class_counts# クラスごとのカバレッジを取得する関数def 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# クラスごとの平均セットサイズを取得する関数def 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     # クラスサイズによって重み付けされたカバレッジを取得する関数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# クラスサイズによって重み付けされたセットサイズを取得する関数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

各クラスの結果を表示します。

results = pd.DataFrame(index=class_labels)results['Class counts'] = get_class_counts(y_test)results['Coverage'] = get_coverage_by_class(prediction_sets, y_test)results['Average set size'] = get_average_set_size(prediction_sets, y_test)results

        クラスの数   カバレッジ   平均セットサイズ
青色    241           0.817427   1.087137
オレンジ  848           0.954009   1.037736
緑色   828           0.977053   1.016908

全体の結果を表示します。

weighted_coverage = get_weighted_coverage(    results['Coverage'], results['Class counts'])weighted_set_size = get_weighted_set_size(    results['Average set size'], results['Class counts'])print (f'全体のカバレッジ:{weighted_coverage}')print (f'平均セットサイズ:{weighted_set_size}')

全体のカバレッジ:0.947平均セットサイズ:1.035

注意:全体のカバレッジは望ましい値であるため、95%に非常に近いですが、異なるクラスのカバレッジは異なり、最も小さいクラスのカバレッジは最も低い(83%)です。個々のクラスのカバレッジが重要な場合、クラスごとにしきい値を設定できます。それが行うことです。

クラスごとのカバレッジが均等になるような一致分類

すべてのクラスでカバレッジを確実にする場合、各クラスのしきい値を設定できます。

注:問題が生じる場合は、診断検査での異なる人種グループ間のカバレッジを均等にするなど、データのサブグループに対しても同様にすることもできます。

クラスごとにしきい値を取得する

# alphaを設定(1-カバレッジ)alpha = 0.05thresholds = []# キャリブレーションセットの予測確率を取得y_cal_prob = classifier.predict_proba(X_Cal)# 各クラスのsスコアの95番目のパーセンタイルを取得for class_label in range(n_classes):    mask = y_cal == class_label    y_cal_prob_class = y_cal_prob[mask][:, class_label]    s_scores = 1 - y_cal_prob_class    q = (1 - alpha) * 100    class_size = mask.sum()    correction = (class_size + 1) / class_size    q *= correction    threshold = np.percentile(s_scores, q)    thresholds.append(threshold)print(thresholds)

[0.9030202125697161, 0.6317149025299887, 0.26033562285411]

クラスごとにしきい値を適用して各クラスの分類を行う

# テストセットのSiスコアを取得predicted_proba = classifier.predict_proba(X_test)si_scores = 1 - predicted_proba# 各クラスについて、各インスタンスがしきい値以下かどうかをチェックprediction_sets = []for i in range(n_classes):    prediction_sets.append(si_scores[:, i] <= thresholds[i])prediction_sets = np.array(prediction_sets).T# 分類セットのラベルを取得し、最初の10個を表示prediction_set_labels = get_prediction_set_labels(prediction_sets, class_labels)# 標準の予測を取得y_pred = classifier.predict(X_test)# 予測結果をまとめるresults_sets = pd.DataFrame()results_sets['実際の値'] = [class_labels[i] for i in y_test]results_sets['ラベル'] = get_prediction_set_labels(prediction_sets, class_labels)results_sets['分類結果'] = [class_labels[i] for i in y_pred]# 最初の10個の結果を表示results_sets.head(10)

  実際の値  ラベル           分類結果0 青色     {青色}            青色1 緑色    {緑色}           緑色2 青色     {青色}            青色3 緑色    {緑色}           緑色4 オレンジ   {オレンジ}          オレンジ5 オレンジ   {オレンジ}          オレンジ6 オレンジ   {オレンジ}          オレンジ7 オレンジ   {青色, オレンジ}    青色8 オレンジ   {オレンジ}          オレンジ9 オレンジ   {オレンジ}          オレンジ

カバレッジを確認し、クラス間でサイズを設定する

現在、全クラスで約95%のカバレッジを持っています。準拠予測方法は、標準的な分類方法よりも少数派クラスのカバレッジを向上させます。

results = pd.DataFrame(index=class_labels)results['クラスの数'] = get_class_counts(y_test)results['カバレッジ'] = get_coverage_by_class(prediction_sets, y_test)results['平均セットサイズ'] = get_average_set_size(prediction_sets, y_test)results

        クラスの数     カバレッジ   平均セットサイズblue    241           0.954357   1.228216orange  848           0.956368   1.139151green   828           0.942029   1.006039

加重カバレッジ = get_weighted_coverage(    results['カバレッジ'], results['クラスの数'])加重セットサイズ = get_weighted_set_size(    results['平均セットサイズ'], results['クラスの数'])print (f'全体のカバレッジ:{加重カバレッジ}')print (f'平均セットサイズ:{加重セットサイズ}')

全体のカバレッジ:0.95平均セットサイズ:1.093

概要

準拠予測は、個々の予測ではなく、インスタンスをセットで分類するために使用されました。2つのクラスの境界にあるインスタンスは、最も高い確率を持つクラスではなく、両方のクラスにラベルが付けられました。

すべてのクラスが同じカバレッジで検出されることが重要な場合、インスタンスの分類のための閾値を個別に設定することができます(この方法は、例えば異なる民族グループ間で同じカバレッジを保証するために、データのサブグループにも使用できます)。

準拠予測は、モデルの予測を変更しません。伝統的な分類方法とは異なる方法でそれらを使用するだけです。伝統的な方法と一緒に使用することもできます。

(すべての画像は著者によるものです)

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

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more