📚

複数の学習済みモデルを評価する②: ROC曲線

2023/08/17に公開

はじめに

学習中であればepoch毎の損失値や精度を確認できますが、学習が終わってしまえばどのようにそれら複数モデルの性能を評価すればよいのか、ということが気になると思います。

files

以前の記事で、学習済みモデルを評価する際のさまざまな評価項目と、実際の1つの学習モデルに対してその評価項目を計算するコードを紹介しました。

https://zenn.dev/ykesamaru/articles/41da74603fd098

上記記事では以下のような評価項目を紹介しました。

  • 精度(Accuracy)
  • F1スコア (F1 Score)
  • モデルの解釈可能性 (Model Interpretability)
  • 混同行列(Confusion Matrix)
  • 適合率(Precision)
  • 再現率(Recall)

今回は、学習済みモデルを評価する際の「評価項目」の1つであるROC曲線について、複数の学習済みモデルに対してのコードを紹介します。

pthファイルをONNX形式のファイルへ変換する

PyTorchで訓練されたモデルを他のプラットフォームやフレームワークで使用する場合、ONNX(Open Neural Network Exchange)形式への変換が必要になることがあります。
複数回の学習トライアルで、pth形式とONNX形式の両方がカレントディレクトリにある場合、どちらかに統一する必要があります。

ここでは、PyTorchの.pthファイルをONNX形式に変換する手順を説明します。

1. 必要なライブラリのインポート

最初に、必要なライブラリをインポートします。

import torch
import torch.onnx
import torchvision.models as models

2. モデルのロード

次に、変換したい.pthファイルからモデルをロードします。

model = CustomModel(embedding_size)  # モデル構造
model.load_state_dict(torch.load('path/to/your_model.pth'))
model.eval()  # 評価モードに設定

3. ダミー入力の作成

ONNXエクスポートには、モデルの入力形状を知る必要があります。ダミーの入力データを作成して、モデルに渡します。

dummy_input = torch.randn(1, 3, 224, 224)  # 例: 1バッチ、3チャンネル、224x224画像

4. ONNX形式へのエクスポート

torch.onnx.export関数を使用して、モデルをONNX形式にエクスポートします。

torch.onnx.export(model, dummy_input, 'path/to/output_model.onnx')

これで、モデルはoutput_model.onnxという名前のファイルにエクスポートされます。

実際の変換コード

指定された複数のONNXモデルに対して、画像の前処理、推論、類似度計算、ROC曲線の計算、AUCの計算を行い、最終的にすべてのモデルのROC曲線を1つのグラフにプロットします。

import torch
import torch.nn as nn
import torch.onnx
import timm
from torchvision import transforms

# モデルの定義
class CustomModel(nn.Module):
    def __init__(self, embedding_dim):
        super(CustomModel, self).__init__()

        # EfficientNet V2の事前学習済みモデルを取得し、trunkとして定義
        self.trunk = timm.create_model('tf_efficientnetv2_b0', pretrained=True)
        num_features = self.trunk.classifier.in_features

        # trunkの最終層にembedding_size次元のembedder層を追加
        self.trunk.classifier = nn.Linear(num_features, embedding_dim)

    def forward(self, x):
        return self.trunk(x)

# 画像の前処理を定義
mean_value = [0.485, 0.456, 0.406]
std_value = [0.229, 0.224, 0.225]
train_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(
        mean=mean_value,
        std=std_value
    )
])

# モデルのファイル名リスト
models_list = [
    'best_model_169epoch_512diml.pth',
    'my_23_best_model.pth',
    '11%_my_24_best_model.pth',
]

embedding_size = 512 # 埋め込み次元数

for model_path in models_list:
    # モデルのインスタンスを作成
    model_structure = CustomModel(embedding_size)
    model_structure.load_state_dict(torch.load(model_path))
    model_structure.eval()

    # ダミーの入力データを作成
    dummy_input = torch.randn(1, 3, 224, 224)

    # 出力ファイル名を作成
    onnx_path = model_path.replace('.pth', '.onnx')

    # .onnx形式でモデルを保存
    torch.onnx.export(model_structure, dummy_input, onnx_path, verbose=True)
    print(f'{model_path} was converted to {onnx_path}')

以下のようにONNX形式のファイルが生成されます。

実装コード

それではONNX形式に変換された各学習済みモデルに対して、ROC曲線を作成するコードを作成します。

"""make_ROC_graph.py"""
import os
import sys
from itertools import combinations

import matplotlib.pyplot as plt
import numpy as np
import onnx
import onnxruntime as ort
import torchvision.transforms as transforms
from PIL import Image
from sklearn.metrics import auc, roc_curve
from tqdm import tqdm

sys.path.append('/home/terms/bin/FACE01_IOT_dev')
from face01lib.utils import Utils  # type: ignore

Utils_obj = Utils()

# 画像の前処理を定義
mean_value = [0.485, 0.456, 0.406]
std_value = [0.229, 0.224, 0.225]
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(
        mean=mean_value,
        std=std_value
    )
])

# 複数のモデル名
model_names = [
    '11%_my_24_best_model.onnx',
    'my_23_best_model.onnx',
    'best_model_169epoch_512diml.onnx'
]

# 推論対象の画像ファイルを取得
image_dir = "predict_test"
image_files = [os.path.join(image_dir, f) for f in os.listdir(image_dir) if f.endswith('.png')]

# 類似度判断の関数
def is_same_person(embedding1, embedding2):
    embedding1 = np.asarray(embedding1).flatten()
    embedding2 = np.asarray(embedding2).flatten()
    norm1 = np.linalg.norm(embedding1)
    norm2 = np.linalg.norm(embedding2)
    if norm1 == 0 or norm2 == 0:
        return 1.0
    cos_sim = np.dot(embedding1, embedding2) / (norm1 * norm2)
    return cos_sim

for model_name in tqdm(model_names):
    # ONNXモデルをロード
    onnx_model = onnx.load(model_name)
    ort_session = ort.InferenceSession(model_name)

    # 署名表示
    for prop in onnx_model.metadata_props:
        if prop.key == "signature":
            print(prop.value)

    # 入力名を取得
    input_name = onnx_model.graph.input[0].name

    # 画像を読み込み、前処理を行い、モデルで推論を行う
    embeddings_dict = []
    i = 0
    for image_file in image_files:
        i += 1
        # 50回に1回、CPU温度を取得
        if i % 50 == 0:
            # CPU温度が72度を超えていたら待機
            Utils_obj.temp_sleep()
            i = 0
        name = os.path.splitext(os.path.basename(image_file))[0]
        # nameを'_'で分割して、ラベルを取得
        name = name.split('_')[0]
        image = Image.open(image_file).convert('RGB')
        image = transform(image)
        image = image.unsqueeze(0)  # バッチ次元を追加
        image = image.numpy()
        embedding = ort_session.run(None, {input_name: image})[0]
        # name: embeddingの辞書を作成
        dict = {name: embedding}
        embeddings_dict.append(dict)

    # embeddings_dictの各要素のペアを作成
    pair = list(combinations(embeddings_dict, 2))
    # pairのkeyが同一ならlabelを1、異なれば0とする
    labels = [1 if list(pair[i][0].keys())[0] == list(pair[i][1].keys())[0] else 0 for i in range(len(pair))]
    # pairの各要素の類似度を計算
    # 結果を格納するための空のリストを初期化
    scores = []

    # pairの要素数分ループ
    for i in range(len(pair)):
        # pair[i]の最初の要素からキーを取得
        key1 = list(pair[i][0].keys())[0]
        # pair[i]の最初の要素から、そのキーに対応する値を取得
        value1 = pair[i][0][key1]
        # pair[i]の二番目の要素からキーを取得
        key2 = list(pair[i][1].keys())[0]
        # pair[i]の二番目の要素から、そのキーに対応する値を取得
        value2 = pair[i][1][key2]
        # is_same_person関数を使用して結果を計算
        score = is_same_person(value1, value2)
        # 結果をリストに追加
        scores.append(score)

    # labels_scoresをもとにROC曲線の計算
    fpr, tpr, thresholds = roc_curve(labels, scores)

    # AUCの計算
    roc_auc = auc(fpr, tpr)

    # ROC曲線の描画
    plt.plot(fpr, tpr, label=f'{model_name} (area = {roc_auc:.2f})')


plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC')
plt.grid(True)
plt.legend(loc="lower right")  # 凡例の表示
plt.show()
input("Enterで処理を終了します")

実行結果

評価

ChatGPTのコードインタープリターを使用して、顔認証システムとしての、各学習モデルをグラフから評価してもらいましょう。

質問😐

顔認証システム用の学習モデルの性能を調べるため、ROC-AUC曲線を作成しました。
グラフと以下の文章から、それぞれのモデルが、顔認証システムに使われる学習モデルとしてどう評価されますか?丁寧に解説してください。

青の線の11%_my_24_best_model.onnxのAUCは0.51です。
オレンジの線のmy_23_best_model.onnxのAUCは0.91です。
緑の線のbest_model_169epoch_512diml.onnxのAUCは0.97です。

回答🤖

もちろんです。以下の3つのモデルに対して、ROC-AUC曲線とAUC値からの評価を丁寧に解説します。

1. 青の線の11%_my_24_best_model.onnx(AUC: 0.51)

AUCが0.51であるこのモデルは、精度がほぼランダムと同じレベルです。このモデルは正例と負例を区別する能力がほとんどありません。したがって、顔認証システムとしての信頼性や効果性は非常に低く、実際の使用には適していないと言えるでしょう。

2. オレンジの線のmy_23_best_model.onnx(AUC: 0.91)

AUCが0.91であるこのモデルは、優れた分類性能を示しています。正例と負例を区別する能力が高いため、多くの場合で効果的に顔を認証できると期待できます。しかし、最高レベルのシステムに比べると、誤識別のリスクが若干高くなる可能性があるため、使用状況や要件に応じて注意が必要です。

  • 実運用の複雑性: 顔認証システムは、さまざまな照明、角度、表情などの変動に対して頑健である必要があります。このモデルがこれらの要因にどれだけ耐えられるか、具体的なテストが必要です。
  • 誤識別のリスク: AUCが1.0ではないため、一部のケースで誤認識する可能性があります。このリスクをどれだけ許容できるかは、システムの用途とセキュリティ要件に依存します。

3. 緑の線のbest_model_169epoch_512diml.onnx(AUC: 0.97)

未知のデータでAUCが0.97と非常に高い値を示しているこのモデルは、顔認証システムとして非常に有望です。顔認証システムとしての要件をほぼ完全に満たしており、誤識別のリスクが非常に低く、高セキュリティな環境での使用も envisaged されるでしょう。商業製品や高セキュリティのシステムにも使用できると考えられます。

ただし、過学習していないか、特定のシチュエーションで失敗しないかなど、更なる検証が必要です。

まとめ

  • 11%_my_24_best_model.onnx: ほぼランダムな性能で、顔認証システムとしては不適。
  • my_23_best_model.onnx: 良好な性能を示すが、最高レベルのシステムには及ばず、使用状況に応じた評価が必要。
  • best_model_169epoch_512diml.onnx: 非常に優れた性能で、多岐にわたる顔認証システムに適用可能。

これらの評価は、顔認証システムの具体的な要件や使用状況に合わせて、さらに詳細な分析とテストを行う必要があります。特に、偽陽性と偽陰性のバランス、実際の運用環境での性能、計算負荷などは、実際のシステム設計時に慎重に検討するべきポイントでしょう。

ChatGPTの評価に対する感想😐

ChatGPTはさらなる検証を求めています。F1スコアなどを指していると思われます。
「高セキュリティな環境での使用も envisaged されるでしょう。」という表現がルー語っぽいですね。

まとめ

今回は、複数の学習済みモデルを評価する際の「評価項目」の1つであるROC曲線について、複数の学習済みモデルに対してのコードを紹介しました。
学習モデルはどんどん溜まっていってしまうので、ひとつこのようなコードと未知のデータセットを用意しておくと、学習モデルの評価が簡単にできて便利ですね。

以上です。ありがとうございました。

Discussion