Python NumPy/TensorFlowユーザーのためのTF.jsテンソル入門:形状変更、データ型、前処理/後処理の実践
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
を用いる必要があります。
テンソルは以下の主要な要素を持ちます。
- Shape (形状): テンソルの各次元の要素数を表す配列です。例えば、
[2, 3, 4]
という形状は、2つの「深さ」、各深さに3つの「行」、各行に4つの「列」を持つテンソルを示します。画像データの場合、一般的な形状として[高さ, 幅, チャンネル数]
や[バッチサイズ, 高さ, 幅, チャンネル数]
が用いられます。 - Dtype (データ型): テンソルに含まれる数値のデータ型です。
float32
,int32
,bool
,string
などがあります。画像認識ではfloat32
が一般的ですが、生ピクセルデータはint32
やuint8
で表現されることもあります。 - Rank (階数): テンソルの次元数です。Shape配列の要素数に等しくなります。スカラーはランク0、ベクトルはランク1、行列はランク2です。
テンソルの生成方法
TensorFlow.jsでは、さまざまな方法でテンソルを生成できます。
基本的なテンソル生成
Pythonの tf.constant
や np.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.fromPixels
は int32
のテンソルを返しますが、多くのモデルは 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();
その他のよく使う操作
- スライスとインデックス指定 (
tf.slice
,tf.gather
): テンソルの一部を抽出します。Pythonのtensor[...]
記法に相当しますが、関数呼び出しになります。 - 軸の並べ替え (
tf.transpose
): テンソルの次元の順序を変更します。HWCとCHW形式の変換などで使用します。 - 要素ごとの演算 (
tf.add
,tf.mul
,tf.div
など): テンソル間の算術演算や、テンソルとスカラー間の演算を行います。正規化などで頻繁に使用します。
画像認識における前処理・後処理の全体像
一般的な画像認識タスク(分類など)では、モデルの入力と出力に対して以下のような処理を行います。
- 画像データの読み込み: HTML要素やファイルから画像データを読み込みます。
- テンソルへの変換:
tf.browser.fromPixels
などを用いてピクセルデータをtf.Tensor
に変換します。 - 前処理:
- モデルが要求するサイズへのリサイズ (
tf.image.resizeBilinear
などを使用)。 - モデルが要求するデータ型への変換 (
tf.cast
,toFloat()
)。 - モデルが要求する値の範囲への正規化(除算、引き算など)。
- バッチ次元の追加 (
tf.expandDims
)。
- モデルが要求するサイズへのリサイズ (
- モデルによる推論: 前処理済みのテンソルをモデルに入力し、出力を得ます。
- 後処理:
- 出力テンソルのデータ型や形状を確認します。
- 分類タスクであれば、出力テンソルにSoftmaxを適用して確率に変換する場合があります(モデルによっては最後の層でSoftmaxが既に適用されている)。
- 最も確率の高いクラスのインデックスや、その確率値を取得します。
- 必要に応じて、インデックスをクラス名にマッピングします。
PythonのKerasユーザーであれば、tf.keras.preprocessing.image.img_to_array
や tf.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の同期的な操作に慣れていると、
await
の使用を忘れてしまいがちです。非同期処理を適切に扱うことが重要です。 - メモリ管理: 前述の通り、TF.jsではテンソルを明示的に解放するか、
tf.tidy
を利用する必要があります。Pythonの自動的なメモリ管理とは異なります。 - 画像データ形式: Pythonの画像処理ライブラリ(Pillow, OpenCV)やNumPyで扱う画像データは、形状やデータ型に様々なバリエーションがありますが、Web環境ではHTML要素からのピクセルデータ取得が起点となることが多く、
tf.browser.fromPixels
の出力形状やデータ型を理解することが重要です。 - APIの差異: 細かい関数名や引数の違いがあります。公式ドキュメントを参照しながら進めるのが確実です。
まとめ
この記事では、TensorFlow.jsにおけるテンソルの基本的な概念から、画像認識タスクで頻繁に利用する形状変更、データ型変換、そして前処理・後処理の実装パターンについて解説しました。Pythonでの機械学習経験をお持ちの読者の方々が、NumPyやTensorFlow/Kerasでの知識をTensorFlow.jsに応用する際の橋渡しとなれば幸いです。
TF.jsでの開発においては、テンソル操作の習得に加え、非同期処理とメモリ管理への配慮が特に重要となります。これらの基礎をしっかりと理解することで、ブラウザやNode.js環境でのAI開発をよりスムーズに進めることができるでしょう。
今後は、これらの基礎を踏まえ、特定のモデルの利用法や、より高度なテンソル操作、パフォーマンス最適化など、具体的な応用例について掘り下げていく予定です。