TF.js 実践開発レシピ

Python NumPy/TensorFlowユーザーのためのTF.jsテンソル入門:形状変更、データ型、前処理/後処理の実践

Tags: TensorFlow.js, 画像認識, テンソル, JavaScript, Python, 前処理, 後処理

TensorFlow.jsを用いた画像認識AI開発において、データの根幹をなすのは「テンソル」です。Pythonで機械学習開発に携わった経験のある方であれば、NumPy配列やTensorFlow/PyTorchのテンソルに馴染みがあることでしょう。JavaScript環境でTensorFlow.jsを扱う上でも、これらのテンソル操作の知識は非常に役立ちますが、同時にJavaScript特有の違いや、Web環境での画像データ扱いの違いを理解する必要があります。

この記事では、Pythonでのテンソル操作経験を持つ読者の方々が、TensorFlow.jsにおけるテンソルの概念、基本的な操作、そして画像認識タスクにおける実践的な前処理・後処理をスムーズに習得できるよう、具体的なコード例を交えて解説します。

TensorFlow.jsにおけるテンソルとは

TensorFlow.jsにおけるテンソル(tf.Tensor)は、多次元配列として数値データを表現するための基本的なデータ構造です。これはPythonにおけるNumPyの ndarray やTensorFlow/PyTorchのTensorと同様の概念です。テンソルは、機械学習モデルの入力、内部処理の中間結果、そして出力として使用されます。

しかし、Pythonのテンソルと決定的に異なる点として、TensorFlow.jsのテンソル操作は通常、非同期で行われます。これはJavaScriptの非同期処理モデルや、GPUなどのハードウェアアクセラレーションを利用する際の制約によるものです。テンソルを生成したり、演算を実行したりする関数は Promise を返し、結果を取得する際には await を用いる必要があります。

テンソルは以下の主要な要素を持ちます。

テンソルの生成方法

TensorFlow.jsでは、さまざまな方法でテンソルを生成できます。

基本的なテンソル生成

Pythonの tf.constantnp.array に相当する tf.tensor 関数が最も一般的です。

import * as tf from '@tensorflow/tfjs';

// スカラー (ランク0)
const scalar = tf.scalar(10);
console.log('Scalar:');
scalar.print(); // 10

// ベクトル (ランク1)
const vector = tf.tensor([1, 2, 3, 4]);
console.log('Vector:');
vector.print();
// Tensor
//     [1, 2, 3, 4]

// 行列 (ランク2)
const matrix = tf.tensor([[1, 2], [3, 4]]);
console.log('Matrix:');
matrix.print();
// Tensor
//     [[1, 2],
//      [3, 4]]

// 3次元テンソル (ランク3)
const tensor3d = tf.tensor([[[1], [2]], [[3], [4]]]);
console.log('3D Tensor:');
tensor3d.print();
// Tensor
//     [[[1],
//       [2]],
//
//      [[3],
//       [4]]]

// 特定の値で埋められたテンソル
const zeros = tf.zeros([2, 3]); // 形状 [2, 3] のゼロテンソル
const ones = tf.ones([4]); // 形状 [4] のイチテンソル

console.log('Zeros Tensor:');
zeros.print();
// Tensor
//     [[0, 0, 0],
//      [0, 0, 0]]

console.log('Ones Tensor:');
ones.print();
// Tensor
//     [1, 1, 1, 1]

// テンソルの情報を取得
console.log('Vector shape:', vector.shape); // [4]
console.log('Matrix shape:', matrix.shape); // [2, 2]
console.log('Matrix dtype:', matrix.dtype); // float32 (デフォルト)
console.log('Matrix rank:', matrix.rank);   // 2

tf.tensor に配列を渡す場合、デフォルトのデータ型は float32 になります。必要に応じて dtype オプションを指定できます。

const intTensor = tf.tensor([1, 2, 3], null, 'int32'); // データ型を int32 に指定
console.log('Int32 Tensor:');
intTensor.print(); // dtype が int32 になっていることを確認

画像データからのテンソル生成

画像認識において最もよく使われるのが、HTMLの <img> 要素や <video> 要素、<canvas> 要素からテンソルを生成する tf.browser.fromPixels 関数です。これはピクセルデータをテンソルに変換し、デフォルトで形状 [高さ, 幅, チャンネル数] (HWC形式) 、データ型 int32 または float32 (正規化した場合) のテンソルを返します。

// HTML内に img 要素があると仮定
// <img id="my-image" src="path/to/your/image.jpg">

async function processImage() {
  const imgElement = document.getElementById('my-image');

  // img 要素からテンソルを生成
  // デフォルトでは形状 [height, width, 3] (RGB), dtype int32
  const imgTensor = tf.browser.fromPixels(imgElement);

  console.log('Image Tensor shape:', imgTensor.shape); // 例: [256, 256, 3]
  console.log('Image Tensor dtype:', imgTensor.dtype); // int32

  // 必要に応じて、データ型を float32 に変換し、値を [0, 1] の範囲に正規化
  const normalizedImgTensor = imgTensor.toFloat().div(255.0);

  console.log('Normalized Image Tensor shape:', normalizedImgTensor.shape); // [256, 256, 3]
  console.log('Normalized Image Tensor dtype:', normalizedImgTensor.dtype); // float32

  // 生成したテンソルは不要になったら解放する
  imgTensor.dispose();
  normalizedImgTensor.dispose();
}

// processImage(); // 画像ロード後に実行するなど

tf.browser.fromPixels は非同期関数ではありませんが、生成されたテンソルは他のテンソルと同様に扱います。

実践的なテンソル操作:画像認識の前処理でよく使うもの

PythonのNumPyやTensorFlow/Kerasで行う画像の前処理は、TF.jsでも同様のテンソル操作関数を使って実現できます。ここでは、特によく使う形状変更やデータ型変換について解説します。

形状変更 (Reshape)

多くの事前学習済みモデルは、入力として特定の形状のテンソルを要求します。特に、単一の画像を推論にかける場合でも、モデルは通常、バッチ処理を想定しており、入力形状の最初にバッチサイズ次元(通常は1)が必要です。tf.reshape 関数を用いてテンソルの形状を変更します。

tf.browser.fromPixels で取得したテンソルは形状 [高さ, 幅, チャンネル数] (HWC) ですが、多くのCNNモデルは入力として形状 [バッチサイズ, 高さ, 幅, チャンネル数] (NHWC) を期待します。この変換は tf.expandDims を使うとより直感的です。

// 形状 [256, 256, 3] の画像テンソルを仮定
const imageTensor = tf.zeros([256, 256, 3]); // 例としてゼロテンソルを使用

console.log('Original shape:', imageTensor.shape); // [256, 256, 3]

// バッチ次元 (サイズ 1) を追加
// axis = 0 は最初の次元に追加することを意味します
const batchedImageTensor = imageTensor.expandDims(0);

console.log('Shape after expandDims(0):', batchedImageTensor.shape); // [1, 256, 256, 3]

// 別の例:特定の形状にリシェイプ
const flatTensor = tf.tensor(tf.randomNormal([24])); // 形状 [24] のテンソルを生成

console.log('Original flat shape:', flatTensor.shape); // [24]

// これを形状 [2, 3, 4] に変更
const reshapedTensor = flatTensor.reshape([2, 3, 4]);

console.log('Reshaped shape:', reshapedTensor.shape); // [2, 3, 4]

flatTensor.dispose();
reshapedTensor.dispose();
imageTensor.dispose();
batchedImageTensor.dispose();

tf.reshape はテンソルの要素数を変えずに形状だけを変更します。tf.expandDims(axis) は、指定した axis の位置にサイズ1の新しい次元を追加します。画像認識では、バッチ次元を追加するために axis=0 を指定することが非常に多いです。

データ型変換 (Cast)

前述の通り、tf.browser.fromPixelsint32 のテンソルを返しますが、多くのモデルは float32 入力を期待します。また、ピクセル値を [0, 1] や [-1, 1] の範囲に正規化する際も、データ型を浮動小数点数に変換する必要があります。tf.cast または toFloat(), toInt() などのメソッドを使用します。

const intImageTensor = tf.tensor([[[[255, 0, 0]]]], [1, 1, 1, 3], 'int32'); // NHWC, int32 例

console.log('Original int Tensor:');
intImageTensor.print(); // [[[[255,   0,   0]]]]

// float32 に変換
const floatImageTensor = intImageTensor.cast('float32');

console.log('Float32 Tensor:');
floatImageTensor.print(); // [[[[255.,   0.,   0.]]]]

// 0-1 の範囲に正規化 (int32 から float32 に変換してから行う)
const normalizedTensor = intImageTensor.toFloat().div(255.0);

console.log('Normalized Tensor:');
normalizedTensor.print(); // [[[[1.,   0.,   0.]]]]

intImageTensor.dispose();
floatImageTensor.dispose();
normalizedTensor.dispose();

その他のよく使う操作

画像認識における前処理・後処理の全体像

一般的な画像認識タスク(分類など)では、モデルの入力と出力に対して以下のような処理を行います。

  1. 画像データの読み込み: HTML要素やファイルから画像データを読み込みます。
  2. テンソルへの変換: tf.browser.fromPixels などを用いてピクセルデータを tf.Tensor に変換します。
  3. 前処理:
    • モデルが要求するサイズへのリサイズ (tf.image.resizeBilinear などを使用)。
    • モデルが要求するデータ型への変換 (tf.cast, toFloat())。
    • モデルが要求する値の範囲への正規化(除算、引き算など)。
    • バッチ次元の追加 (tf.expandDims)。
  4. モデルによる推論: 前処理済みのテンソルをモデルに入力し、出力を得ます。
  5. 後処理:
    • 出力テンソルのデータ型や形状を確認します。
    • 分類タスクであれば、出力テンソルにSoftmaxを適用して確率に変換する場合があります(モデルによっては最後の層でSoftmaxが既に適用されている)。
    • 最も確率の高いクラスのインデックスや、その確率値を取得します。
    • 必要に応じて、インデックスをクラス名にマッピングします。

PythonのKerasユーザーであれば、tf.keras.preprocessing.image.img_to_arraytf.keras.applications.mobilenet_v2.preprocess_input といった関数を使った経験があると思いますが、TF.jsではこれらの処理を個別のテンソル操作関数を組み合わせて実装することになります。

以下に、一般的な画像分類モデル(入力形状 [1, 高さ, 幅, チャンネル数] (NHWC), 入力値範囲 [0, 1] の float32)を想定した前処理のコード例を示します。

import * as tf from '@tensorflow/tfjs';

async function preprocessForModel(imgElement: HTMLImageElement, targetSize: [number, number]): Promise<tf.Tensor> {
  let imgTensor = tf.browser.fromPixels(imgElement); // 形状 [H, W, C], dtype int32

  // tf.tidy でテンソルメモリを自動管理
  return tf.tidy(() => {
    // ターゲットサイズにリサイズ
    const resizedTensor = tf.image.resizeBilinear(imgTensor, targetSize); // 形状 [targetH, targetW, C], dtype float32 (リサイズ関数はデフォルトでfloat32を返す)

    // データ型を確認 (resizeBilinearは通常float32を返すため不要な場合も)
    const floatTensor = resizedTensor.toFloat(); // float32 に変換

    // 値を [0, 1] の範囲に正規化 (ピクセル値 0-255 を 255 で割る)
    const normalizedTensor = floatTensor.div(255.0); // float32

    // バッチ次元を追加
    const batchedTensor = normalizedTensor.expandDims(0); // 形状 [1, targetH, targetW, C]

    // 元の imgTensor は tf.tidy の管理外だが、この関数内では dispose しない
    // 呼び出し元で必要に応じて imgTensor を dispose するか、tf.tidy で囲む

    return batchedTensor; // 前処理済みのテンソルを返す
  }); // tf.tidy が batchedTensor 以外の全てのテンソルを解放
}

// 後処理の例:分類結果の取得
async function postprocessClassification(outputTensor: tf.Tensor): Promise<{ classIndex: number, probability: number }> {
  // tf.tidy でテンソルメモリを自動管理
  return await tf.tidy(async () => {
    // 出力がlogitの場合、Softmaxを適用して確率に変換
    const probabilities = outputTensor.softmax(); // 形状 [1, num_classes]

    // 最大確率とそのインデックスを取得 (非同期操作)
    const result = await probabilities.argMax(1).data(); // 形状 [1] の int32 配列
    const classIndex = result[0];

    const probabilityData = await probabilities.data(); // 形状 [num_classes] の float32 配列
    const probability = probabilityData[classIndex];

    return { classIndex, probability };
  });
}

// 使用例(非同期関数内で await を使用)
// async function runInference() {
//   const imgElement = document.getElementById('my-image');
//   const model = await tf.loadGraphModel('path/to/model.json'); // モデルのロード

//   // 前処理
//   const inputTensor = await preprocessForModel(imgElement, [224, 224]); // 例:224x224にリサイズ

//   // 推論
//   const outputTensor = model.predict(inputTensor) as tf.Tensor;

//   // 後処理
//   const classificationResult = await postprocessClassification(outputTensor);

//   console.log(`Predicted Class Index: ${classificationResult.classIndex}, Probability: ${classificationResult.probability}`);

//   // 不要になったテンソルを解放 (preprocessForModel, postprocessClassification 内で dispose されていないもの)
//   inputTensor.dispose();
//   outputTensor.dispose();
// }

// runInference(); // 実行例

上記のコード例では、tf.tidy を使用しています。これは、そのコールバック関数内で生成されたテンソルを自動的に解放してくれる便利な機能です。TF.jsでは、テンソルを手動で dispose() するか、tf.tidy を活用してメモリを管理することが非常に重要です。PythonのTensorFlow/PyTorchのようにガベージコレクションに任せきりにすると、特にブラウザ環境ではメモリリークが発生しやすいため注意が必要です。

Pythonとの比較における注意点

まとめ

この記事では、TensorFlow.jsにおけるテンソルの基本的な概念から、画像認識タスクで頻繁に利用する形状変更、データ型変換、そして前処理・後処理の実装パターンについて解説しました。Pythonでの機械学習経験をお持ちの読者の方々が、NumPyやTensorFlow/Kerasでの知識をTensorFlow.jsに応用する際の橋渡しとなれば幸いです。

TF.jsでの開発においては、テンソル操作の習得に加え、非同期処理とメモリ管理への配慮が特に重要となります。これらの基礎をしっかりと理解することで、ブラウザやNode.js環境でのAI開発をよりスムーズに進めることができるでしょう。

今後は、これらの基礎を踏まえ、特定のモデルの利用法や、より高度なテンソル操作、パフォーマンス最適化など、具体的な応用例について掘り下げていく予定です。