TF.js 実践開発レシピ

TensorFlow.jsにおける画像分類モデルの評価方法:ブラウザでの混同行列と精度計算

Tags: TensorFlow.js, 画像分類, モデル評価, 混同行列, 精度

TensorFlow.jsを使用した画像認識AI開発において、モデルの性能を正確に把握することは非常に重要です。特に、画像分類タスクにおいては、訓練データに対する性能だけでなく、未知のデータに対する汎化性能を評価データセットを用いて検証する必要があります。

Pythonによる機械学習開発に慣れている方であれば、TensorFlow/Kerasの model.evaluate() メソッドや、scikit-learnの豊富な評価指標計算機能を利用されていることと存じます。しかし、TensorFlow.jsをブラウザ環境やNode.js環境で利用する場合、評価用データセットの扱い方や、評価指標を計算するためのアプローチはPython環境とは異なります。

本記事では、TensorFlow.jsを用いて画像分類モデルの精度を評価する方法に焦点を当て、ブラウザ環境における評価データセットの準備から、推論の実行、そして混同行列や精度といった主要な評価指標を計算するための具体的なコード例と解説を提供いたします。

TensorFlow.jsにおけるモデル評価の基本的な考え方

モデル評価の目的は、訓練に使用していない独立したデータセット(評価データセット)を用いて、モデルがどの程度正しく予測できるか、あるいはどのような種類の誤りを犯しやすいかを知ることにあります。画像分類タスクの場合、これは評価用画像がどのクラスに分類されるかをモデルに推論させ、その結果が正解ラベルと一致するかどうかを確認するプロセスになります。

Python環境では、評価用データセットをファイルシステムから容易に読み込み、NumPy配列やTensorFlowのDatasetオブジェクトとして効率的に扱えます。また、評価指標の計算ライブラリも充実しています。

一方、TensorFlow.jsをブラウザ環境で実行する場合、評価用画像データはWebサーバーから取得するか、ユーザーのローカルファイルシステムからブラウザのAPI(例: FileReader)経由で読み込む必要があります。データセット全体を一度にメモリにロードすることが難しい場合もあり、非同期処理やストリーミング的なアプローチが必要になることがあります。

評価指標の計算についても、TensorFlow.js自体には evaluate のような高レベルな機能は提供されていません。モデルの推論結果(通常はクラスごとの確率を示すテンソル)と正解ラベルを比較し、JavaScriptの配列操作や、必要に応じてTensorFlow.jsのテンソル演算を組み合わせて、評価指標を自前で計算する必要があります。

評価用データセットの準備と読み込み

ブラウザ環境で評価用データセットを扱うための一般的なアプローチは、評価用画像のパス(URLまたはファイルパス)とそれぞれの正解クラスラベルを、JSONファイルのような構造化されたデータとして管理することです。

例えば、以下のようなJSONファイルを用意します。

[
  {"imagePath": "/eval_data/cat_001.jpg", "label": "cat"},
  {"imagePath": "/eval_data/dog_001.jpg", "label": "dog"},
  {"imagePath": "/eval_data/cat_002.jpg", "label": "cat"},
  // ... 他の評価用データ
]

このJSONファイルをブラウザからHTTPリクエストで読み込み、各エントリーに対して以下の処理を行います。

  1. imagePath から画像を読み込む。URLの場合は fetchImage オブジェクトを使用します。
  2. 読み込んだ画像をTensorFlow.jsで扱えるテンソル形式に変換する(tf.browser.fromPixelsを使用)。
  3. モデルの入力要件に合わせて、テンソルを前処理する(リサイズ、正規化、チャンネル次元の追加など)。この前処理は、モデル訓練時や推論時と全く同じである必要があります。
  4. 正解ラベルを数値IDに変換するなど、モデルの出力形式と対応できるように準備する。
// 例: 評価用データリストを非同期で読み込み、前処理済みテンソルとラベルのリストを作成
async function loadEvalData(evalDataListJsonUrl, imageSize) {
  const response = await fetch(evalDataListJsonUrl);
  const evalData = await response.json();

  const processedData = [];
  const labels = [];

  for (const item of evalData) {
    try {
      // 画像の読み込み
      const imgElement = new Image();
      imgElement.src = item.imagePath;
      await new Promise((resolve, reject) => {
        imgElement.onload = resolve;
        imgElement.onerror = reject;
      });

      // 画像をテンソルに変換
      const imageTensor = tf.browser.fromPixels(imgElement);

      // 前処理(例: リサイズ、正規化、バッチ次元追加)
      // モデルが期待する入力形状に合わせます
      const resizedTensor = tf.image.resizeBilinear(imageTensor, [imageSize, imageSize]);
      const normalizedTensor = resizedTensor.div(255.0); // 0-1に正規化の例
      const inputTensor = normalizedTensor.expandDims(0); // バッチ次元追加

      processedData.push(inputTensor);
      labels.push(item.label);

      // 不要になったテンソルを解放 (メモリリーク防止)
      imageTensor.dispose();
      resizedTensor.dispose();
      normalizedTensor.dispose();

    } catch (error) {
      console.error(`Failed to load or process image: ${item.imagePath}`, error);
      // エラーが発生したデータはスキップするなどの対応が必要
    }
  }

  // 複数のテンソルを結合して一つのバッチテンソルにする場合はここで処理
  // 例: const batchedTensors = tf.concat(processedData, 0);
  // この例では個別データとして扱います

  return { processedData, labels };
}

// この関数は例であり、大規模データセットの場合は異なるアプローチが必要になります。
// 例えば、データをチャンクに分けてロードしたり、推論時に個別に読み込んだりします。

モデルによる推論と結果の格納

評価用データセットを準備したら、次に各データに対してモデルの推論を実行します。PythonのKeras predict メソッドのように、データセット全体を一度に渡して推論することも可能ですが、ブラウザ環境ではメモリの制約が大きいため、一般的にはデータを小分けにして推論を実行するか、1つずつ非同期で処理するのが現実的です。

async function evaluateModel(model, evalData, classNames) {
  const predictions = [];
  const trueLabels = [];

  for (let i = 0; i < evalData.processedData.length; i++) {
    const inputTensor = evalData.processedData[i];
    const trueLabel = evalData.labels[i];

    try {
      tf.tidy(() => { // tf.tidy でメモリ管理を効率化
        const outputTensor = model.predict(inputTensor);
        // 出力テンソルから確率分布を取得
        const predictionProbabilities = outputTensor.dataSync(); // 同期的にJS配列に変換

        predictions.push(predictionProbabilities);
        trueLabels.push(trueLabel);
      }); // tf.tidy の終わりでテンソルが解放されます

      // 不要になった入力テンソルもここで解放(上記 loadEvalData でまとめて返した場合は必要)
      // inputTensor.dispose();

    } catch (error) {
      console.error(`Failed prediction for data ${i}:`, error);
      // エラー処理
    }
  }

  // processedDataのテンソルも解放
  evalData.processedData.forEach(t => t.dispose());

  return { predictions, trueLabels };
}

上記の例では、各データに対して個別に model.predict を実行していますが、可能な場合は複数のデータをまとめてバッチとして推論することでパフォーマンスが向上します。

評価指標の計算:混同行列と基本指標

推論結果と正解ラベルが得られたら、それらを基に評価指標を計算します。画像分類タスクで最も基本的な評価指標は「精度 (Accuracy)」ですが、クラス Imbalance(クラス間のデータ数に偏りがあること)がある場合は、適合率 (Precision)、再現率 (Recall)、F1スコアなども考慮する必要があります。これらの指標は通常、混同行列 (Confusion Matrix) から計算されます。

混同行列

混同行列は、各クラスについて、モデルが実際に予測したクラスと真のクラスの数をまとめた表です。行が真のクラス、列が予測されたクラスを表します。

例えば、3クラス分類(猫, 犬, 鳥)の場合、混同行列は以下のようになります。

| 真のクラス \ 予測クラス | 猫を予測 | 犬を予測 | 鳥を予測 | 合計 | | :---------------------- | :------- | :------- | :------- | :--- | | 真のクラス:猫 | 真陽性(TP) | 偽陰性(FN) | 偽陰性(FN) | 猫の総数 | | 真のクラス:犬 | 偽陽性(FP) | 真陽性(TP) | 偽陰性(FN) | 犬の総数 | | 真のクラス:鳥 | 偽陽性(FP) | 偽陽性(FP) | 真陽性(TP) | 鳥の総数 | | 合計 | 猫と予測した総数 | 犬と予測した総数 | 鳥と予測した総数 | データ総数 |

対角線上の要素が正しく分類された数(真陽性 True Positive, TP)を示します。

混同行列を計算するためのコード例を示します。まず、予測されたクラスと真のクラスを数値IDに変換する必要があります。

function calculateConfusionMatrix(predictions, trueLabels, classNames) {
  const numClasses = classNames.length;
  const confusionMatrix = Array(numClasses).fill(0).map(() => Array(numClasses).fill(0));

  // クラス名からIDへのマッピングを作成
  const classMap = new Map(classNames.map((name, index) => [name, index]));

  for (let i = 0; i < predictions.length; i++) {
    const trueLabel = trueLabels[i];
    const predictionProbabilities = predictions[i];

    // 予測されたクラスIDを決定 (確率が最大のクラス)
    const predictedClassId = predictionProbabilities.indexOf(
      Math.max(...predictionProbabilities)
    );

    const trueClassId = classMap.get(trueLabel);

    if (trueClassId !== undefined) {
      // 混同行列を更新
      confusionMatrix[trueClassId][predictedClassId]++;
    } else {
      console.warn(`Unknown true label: ${trueLabel}`);
    }
  }

  return confusionMatrix;
}

基本的な評価指標の計算

混同行列が計算できれば、以下の基本的な評価指標を計算できます。ここで、TP_i, FP_i, FN_i はそれぞれクラス i に対する真陽性、偽陽性、偽陰性の数を示します。

これらの指標を計算する関数を作成できます。

function calculateEvaluationMetrics(confusionMatrix, classNames) {
  const numClasses = classNames.length;
  const metrics = {
    accuracy: 0,
    precision: [],
    recall: [],
    f1Score: []
  };

  let totalCorrect = 0;
  let totalSamples = 0;

  for (let i = 0; i < numClasses; i++) {
    const TP = confusionMatrix[i][i];
    let FP = 0; // iと予測されたが実際は違うクラス
    let FN = 0; // iではなかったがiと予測されなかった (実際はiなのに違うと予測)

    totalCorrect += TP;
    totalSamples += confusionMatrix[i].reduce((sum, val) => sum + val, 0); // 行の合計

    // FPとFNを計算
    for (let j = 0; j < numClasses; j++) {
      if (i !== j) {
        FP += confusionMatrix[j][i]; // クラスjだがクラスiと予測された
        FN += confusionMatrix[i][j]; // クラスiだがクラスjと予測された
      }
    }

    // 適合率、再現率、F1スコアの計算(分母が0になる可能性に注意)
    const precision = (TP + FP) === 0 ? 0 : TP / (TP + FP);
    const recall = (TP + FN) === 0 ? 0 : TP / (TP + FN);
    const f1Score = (precision + recall) === 0 ? 0 : 2 * (precision * recall) / (precision + recall);

    metrics.precision.push(precision);
    metrics.recall.push(recall);
    metrics.f1Score.push(f1Score);
  }

  metrics.accuracy = totalSamples === 0 ? 0 : totalCorrect / totalSamples;

  // クラスごとの指標をオブジェクト形式で返すなど、より分かりやすい形式に整形することも可能
  // 例: { className: { precision: ..., recall: ..., f1Score: ... }, ... }

  return metrics;
}

Pythonのscikit-learnの classification_report のように、各クラスごとの適合率、再現率、F1スコア、およびサポート数(そのクラスの真のデータ数)を一覧で表示したり、マイクロ平均、マクロ平均を計算したりすることも可能です。これらの発展的な計算も、上記の基本的な計算ロジックを応用することで実装できます。

実践上の考慮事項

まとめ

本記事では、TensorFlow.jsを用いてブラウザ環境で画像分類モデルの精度を評価する方法について解説しました。Python環境での評価との違いを認識しつつ、評価用データセットの準備、モデルによる推論、そして混同行列から精度や適合率、再現率といった評価指標を計算するための具体的なコード例を示しました。

ブラウザ上でのモデル評価は、メモリや非同期処理に関する考慮事項がPython環境とは異なりますが、適切なアプローチをとることで、TensorFlow.jsで開発した画像認識モデルの性能をクライアントサイドで正確に把握し、その後のモデル改善やデプロイ判断に役立てることが可能です。

ここで紹介した内容は基本的なものですが、これを応用することで、さらに詳細な分析(例: 特定のクラスで誤分類が多い原因の調査)や、他の評価指標(例: ROC曲線、AUCなど)の計算にも繋げることができます。実践的な開発において、モデルの訓練だけでなく、その評価プロセスもしっかりと構築することをお勧めいたします。