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リクエストで読み込み、各エントリーに対して以下の処理を行います。
imagePath
から画像を読み込む。URLの場合はfetch
やImage
オブジェクトを使用します。- 読み込んだ画像をTensorFlow.jsで扱えるテンソル形式に変換する(
tf.browser.fromPixels
を使用)。 - モデルの入力要件に合わせて、テンソルを前処理する(リサイズ、正規化、チャンネル次元の追加など)。この前処理は、モデル訓練時や推論時と全く同じである必要があります。
- 正解ラベルを数値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
に対する真陽性、偽陽性、偽陰性の数を示します。
-
精度 (Accuracy): 全データのうち、正しく分類されたデータの割合。 $$ \text{Accuracy} = \frac{\sum_{i=0}^{N-1} TP_i}{\text{総データ数}} $$ 混同行列の対角成分の合計を、行列全体の合計で割ることで計算できます。
-
適合率 (Precision): あるクラスと予測されたデータのうち、実際にそのクラスであったデータの割合。 $$ \text{Precision}_i = \frac{TP_i}{TP_i + FP_i} $$ 混同行列において、クラス
i
の適合率は、クラスi
の列の対角成分($TP_i$)を、その列の合計($TP_i + FP_i$)で割ることで計算できます。 -
再現率 (Recall): あるクラスに属するデータのうち、正しくそのクラスと予測されたデータの割合(検出率とも呼ばれます)。 $$ \text{Recall}_i = \frac{TP_i}{TP_i + FN_i} $$ 混同行列において、クラス
i
の再現率は、クラスi
の行の対角成分($TP_i$)を、その行の合計($TP_i + FN_i$)で割ることで計算できます。 -
F1スコア (F1-score): 適合率と再現率の調和平均。適合率と再現率のバランスを見る指標です。 $$ \text{F1-score}_i = 2 \times \frac{\text{Precision}_i \times \text{Recall}_i}{\text{Precision}_i + \text{Recall}_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スコア、およびサポート数(そのクラスの真のデータ数)を一覧で表示したり、マイクロ平均、マクロ平均を計算したりすることも可能です。これらの発展的な計算も、上記の基本的な計算ロジックを応用することで実装できます。
実践上の考慮事項
- メモリ管理: ブラウザ環境ではメモリが限られています。評価データセットをまとめてロードしたり、推論結果を大量にメモリに保持したりすると、タブがクラッシュする可能性があります。データを小分けにして処理したり、
tf.dispose()
やtf.tidy()
を適切に使用して不要になったテンソルを解放したりすることが非常に重要です。 - 非同期処理: 画像の読み込みや推論は時間のかかる処理です。UIスレッドをブロックしないように、
async/await
を活用し、非同期処理を徹底してください。 - Web Worker: 大量の推論処理を実行する場合、UIスレッドとは別のWeb Worker内でTensorFlow.jsを実行することで、ブラウザの応答性を維持できます。
- データセットの規模: 評価データセットが非常に大きい場合、ブラウザで全てを処理するのは非現実的です。その場合は、評価の一部をサーバーサイドで行う、あるいはサンプリングして評価を行うといったアプローチも検討する必要があります。
- 結果の表示: 計算した評価指標は、ユーザーや開発者にとって分かりやすい形で表示することが望ましいです。混同行列をテーブルで表示したり、指標をリストアップしたりするUIの実装が必要です。
まとめ
本記事では、TensorFlow.jsを用いてブラウザ環境で画像分類モデルの精度を評価する方法について解説しました。Python環境での評価との違いを認識しつつ、評価用データセットの準備、モデルによる推論、そして混同行列から精度や適合率、再現率といった評価指標を計算するための具体的なコード例を示しました。
ブラウザ上でのモデル評価は、メモリや非同期処理に関する考慮事項がPython環境とは異なりますが、適切なアプローチをとることで、TensorFlow.jsで開発した画像認識モデルの性能をクライアントサイドで正確に把握し、その後のモデル改善やデプロイ判断に役立てることが可能です。
ここで紹介した内容は基本的なものですが、これを応用することで、さらに詳細な分析(例: 特定のクラスで誤分類が多い原因の調査)や、他の評価指標(例: ROC曲線、AUCなど)の計算にも繋げることができます。実践的な開発において、モデルの訓練だけでなく、その評価プロセスもしっかりと構築することをお勧めいたします。