TF.js 実践開発レシピ

TensorFlow.jsで画像認識におけるデータ拡張を実装する:Python Kerasからの視点

Tags: TensorFlow.js, 画像認識, データ拡張, Keras, JavaScript

画像認識モデルの学習において、データ拡張(Data Augmentation)はモデルの汎化性能を高め、過学習を抑制するために非常に重要な手法です。入力画像をランダムに変換(回転、拡大縮小、反転、クロップなど)することで、学習データセットの見かけ上のサイズを増やし、様々なバリエーションのデータに対してモデルが頑健になるように学習を進めます。

PythonでTensorFlowやKerasを用いて画像認識モデルを開発されている方は、tf.keras.preprocessing.image.ImageDataGeneratortf.data.Datasettf.image モジュールを組み合わせてデータ拡張を実装されていることと思います。TensorFlow.jsでも、これらの手法をJavaScriptの環境で実現することが可能です。

本記事では、TensorFlow.jsを用いて画像認識のためのデータ拡張を実装する具体的なコード例と、Python Kerasでの実装との関連性について解説します。

なぜTensorFlow.jsでデータ拡張を実装するのか

データ拡張は主にモデルの「学習時」に行われる前処理ですが、学習済みのモデルをTensorFlow.jsで利用する場合でも、推論前に学習時と同じ前処理(リサイズ、正規化など)を行う必要があります。また、ブラウザやNode.js環境で少量データのファインチューニングや転移学習を行う場合、その環境でデータ拡張を実装する必要が出てきます。

Pythonで慣れ親しんだデータ拡張の概念をTensorFlow.jsにどう落とし込むかを知ることは、TensorFlow.jsでの実践的な開発を進める上で役立ちます。

TensorFlow.jsにおける画像テンソルの扱い

データ拡張を行うためには、まず画像をTensorFlow.jsのテンソルとして扱う必要があります。ブラウザ環境では tf.browser.fromPixels、Node.js環境では tf.node.decodeImage などを使用して画像データをテンソルに変換します。一般的な画像認識モデルの入力は、バッチサイズ、高さ、幅、チャンネル数の4次元テンソル [batch_size, height, width, channels] です。

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

// ブラウザ環境での例 (Canvas要素から画像データを取得)
async function loadImageFromCanvas(canvasId) {
  const canvas = document.getElementById(canvasId);
  if (!canvas) {
    console.error('Canvas element not found.');
    return null;
  }
  // Canvasの内容をRGBテンソルに変換
  // fromPixelsは[height, width, channels]の3次元テンソルを返します
  const imgTensor = tf.browser.fromPixels(canvas);
  // モデルの入力に合わせてバッチ次元を追加する場合
  // const imgTensorWithBatch = imgTensor.expandDims(0);
  return imgTensor;
}

// Node.js環境での例 (ファイルパスから画像を読み込み)
// Node.js環境では '@tensorflow/tfjs-node' または '@tensorflow/tfjs-node-gpu' が必要です
// import * as fs from 'fs';
// async function loadImageFromFile(filePath) {
//   const imageBuffer = fs.readFileSync(filePath);
//   // decodeImageはPromiseを返します
//   const imgTensor = tf.node.decodeImage(imageBuffer);
//   // モデルの入力に合わせてバッチ次元を追加する場合
//   // const imgTensorWithBatch = imgTensor.expandDims(0);
//   return imgTensor;
// }

TensorFlow.jsで代表的なデータ拡張を実装する

TensorFlow.jsには、画像関連の操作を行うための便利な関数が tf.image モジュールに用意されています。これらを活用してデータ拡張を実装します。

1. リサイズ (Resize)

画像認識モデルの多くは、特定の入力サイズを要求します。リサイズはデータ拡張そのものというよりは、必須の前処理です。

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

/**
 * 画像テンソルを指定されたサイズにリサイズする
 * @param {tf.Tensor3D} imageTensor - [height, width, channels]の画像テンソル
 * @param {number} targetHeight - リサイズ後の高さ
 * @param {number} targetWidth - リサイズ後の幅
 * @returns {tf.Tensor3D} リサイズ後の画像テンソル
 */
function resizeImage(imageTensor, targetHeight, targetWidth) {
  // method: 'bilinear' (デフォルト), 'nearest', 'bicubic'
  // alignCorners: true/false
  return tf.image.resizeBilinear(imageTensor, [targetHeight, targetWidth]);
}

// 使用例
// assume originalImageTensor is a tf.Tensor3D [height, width, channels]
// const resizedTensor = resizeImage(originalImageTensor, 224, 224);

2. 反転 (Flip)

画像を左右または上下にランダムに反転させることは、多くの画像認識タスクで有効です。

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

/**
 * 画像テンソルをランダムに左右または上下に反転する
 * @param {tf.Tensor3D} imageTensor - [height, width, channels]の画像テンソル
 * @param {string} [direction='horizontal'] - 'horizontal' または 'vertical'
 * @returns {tf.Tensor3D} 変換後の画像テンソル
 */
function randomFlip(imageTensor, direction = 'horizontal') {
  if (direction === 'horizontal') {
    // ランダムに左右反転
    return tf.image.randomFlipLeftRight(imageTensor);
  } else if (direction === 'vertical') {
    // ランダムに上下反転
    return tf.image.randomFlipUpDown(imageTensor);
  } else {
    console.warn('Invalid direction for randomFlip. Returning original tensor.');
    return imageTensor;
  }
}

// 使用例
// const flippedTensor = randomFlip(imageTensor, 'horizontal');

3. 回転 (Rotate)

画像を特定の角度で回転させます。tf.image.rotate は任意角度の回転をサポートします。

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

/**
 * 画像テンソルをラジアンで指定された角度だけ回転する
 * @param {tf.Tensor3D} imageTensor - [height, width, channels]の画像テンソル
 * @param {number} angleRadians - 回転角度(ラジアン)
 * @returns {tf.Tensor3D} 変換後の画像テンソル
 */
function rotateImage(imageTensor, angleRadians) {
  // fillValue: 回転によって生じる空白部分を埋める値 (0-255)
  // center: 回転の中心 [x, y]。デフォルトは画像中心
  return tf.image.rotate(imageTensor, angleRadians);
}

/**
 * 画像テンソルをランダムな角度(度数法)で回転する
 * @param {tf.Tensor3D} imageTensor - [height, width, channels]の画像テンソル
 * @param {number} maxAngleDegrees - 最大回転角度(-maxAngleDegrees から +maxAngleDegrees の範囲)
 * @returns {tf.Tensor3D} 変換後の画像テンソル
 */
function randomRotate(imageTensor, maxAngleDegrees) {
    const angleDegrees = (Math.random() * 2 - 1) * maxAngleDegrees; // -max から +max の範囲
    const angleRadians = angleDegrees * Math.PI / 180; // 度数法をラジアンに変換
    return rotateImage(imageTensor, angleRadians);
}

// 使用例
// const rotatedTensor = randomRotate(imageTensor, 15); // -15度から+15度の範囲でランダム回転

4. ランダムクロップとリサイズ (Random Crop and Resize)

画像をランダムにクロップし、元のサイズにリサイズすることで、オブジェクトの位置やスケールのバリエーションを増やします。Python Kerasの random_crop に相当します。

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

/**
 * 画像をランダムにクロップし、元のサイズにリサイズする
 * @param {tf.Tensor3D} imageTensor - [height, width, channels]の画像テンソル
 * @param {number} targetHeight - リサイズ後の高さ
 * @param {number} targetWidth - リサイズ後の幅
 * @param {number} minScale - クロップ領域の元の画像に対する最小スケール (0-1)
 * @param {number} maxScale - クロップ領域の元の画像に対する最大スケール (0-1)
 * @returns {tf.Tensor3D} 変換後の画像テンソル
 */
function randomCropAndResize(imageTensor, targetHeight, targetWidth, minScale, maxScale) {
  const [height, width, channels] = imageTensor.shape;

  // クロップする領域の相対座標 ([y1, x1, y2, x2] 形式) をランダムに生成
  // tf.image.randomCropAndResize はバッチ入力に対応するため、batch_indices と boxes が必要
  const numBoxes = 1; // 単一画像なので1
  const boxIndices = tf.tensor1d([0], 'int32'); // バッチ内のインデックス (単一画像なので0)
  const cropSize = [targetHeight, targetWidth]; // クロップ後のリサイズサイズ

  // TensorFlow.jsのrandomCropAndResizeはminScale/maxScaleを引数に取らないため、
  // 自分でボックス座標を生成する必要がある。
  // または tf.image.cropAndResize を使用し、ランダムなbox座標を生成する。

  // より簡単な方法として、Pythonのtf.image.random_crop_and_resize に近い動作を
  // tf.image.cropAndResize とランダム座標生成で実現する。
  // tf.image.randomCropAndResizeV2 もありますが、少し複雑なため、ここでは簡単な例を示します。

  // ここでは、tf.image.randomCropAndResize を使用します (単一画像対応の簡易版として利用)
  // この関数は内部でランダムなボックス座標を生成します。
  // [TODO: TensorFlow.jsのrandomCropAndResizeはPythonのそれと引数が異なります。
  //  よりPythonライクな実装には、別途ランダム座標生成ロジックが必要です。
  //  以下は tf.image.randomCropAndResize の直接利用例です。]

  // 注意: tf.image.randomCropAndResize はバッチ入力を想定しており、
  // 単一画像の場合は batch_size=1 として shape を調整する必要があるかもしれません。
  // または、自分でランダムなクロップボックスを計算し、tf.image.cropAndResize を使う方が制御しやすいです。

  // 簡易実装例 (tf.image.randomCropAndResizeの使用) - 要検証
  // この関数は主に tf.data パイプラインでの使用を想定しています。
  // const batchedImage = imageTensor.expandDims(0); // バッチ次元を追加
  // const croppedAndResized = tf.image.randomCropAndResize(
  //   batchedImage, // バッチ入力
  //   [height, width], // image_size (元の画像のサイズ)
  //   cropSize, // crop_size (最終的な出力サイズ)
  //   minScale, // scale_ratio_range [min, max]
  //   maxScale
  // );
  // return croppedAndResized.squeeze(0); // バッチ次元を削除

  // より制御しやすい手動での random crop + resize 例
  // 以下のロジックは簡易版です。アスペクト比を保つ、領域が画像からはみ出さないなどの考慮が必要です。
  const scale = Math.random() * (maxScale - minScale) + minScale;
  const cropHeight = Math.floor(height * scale);
  const cropWidth = Math.floor(width * scale);

  if (cropHeight > height || cropWidth > width) {
      console.warn("Generated crop size is larger than the image. Skipping crop.");
      return resizeImage(imageTensor, targetHeight, targetWidth); // リサイズのみ適用
  }

  const yOffset = Math.floor(Math.random() * (height - cropHeight));
  const xOffset = Math.floor(Math.random() * (width - cropWidth));

  // cropAndResizeに必要な引数を準備
  const boxes = tf.tensor2d([[yOffset / height, xOffset / width, (yOffset + cropHeight) / height, (xOffset + cropWidth) / width]]);
  const boxInd = tf.tensor1d([0], 'int32'); // バッチ内のインデックス
  const cropSizeTensor = tf.tensor1d([targetHeight, targetWidth], 'int32');

  const croppedAndResized = tf.image.cropAndResize(imageTensor.expandDims(0), boxes, boxInd, cropSizeTensor);

  return croppedAndResized.squeeze(0); // バッチ次元を削除
}

// 使用例 (入力画像を256x256からランダムクロップ&リサイズして224x224にする場合)
// const originalSize = 256;
// const targetSize = 224;
// const minScale = 0.8;
// const maxScale = 1.0;
// assume imageTensor is a tf.Tensor3D [originalSize, originalSize, channels]
// const croppedAndResizedTensor = randomCropAndResize(imageTensor, targetSize, targetSize, minScale, maxScale);

この randomCropAndResize の手動実装例は、Pythonの tf.image.random_crop_and_resize の概念に近い動作をTF.jsの tf.image.cropAndResize を使って実現するものです。ランダムなクロップ領域を計算し、その領域を抽出して指定サイズにリサイズします。

5. 色の変換 (Color Jittering)

明るさ、コントラスト、色相、彩度をランダムに変更することで、照明や色の変化に対するモデルの耐性を高めます。

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

/**
 * 画像テンソルの明るさをランダムに変更する
 * @param {tf.Tensor3D} imageTensor - [height, width, channels]の画像テンソル (0-1または0-255の範囲)
 * @param {number} maxDelta - 明るさの最大変更量 (0-1)
 * @returns {tf.Tensor3D} 変換後の画像テンソル
 */
function randomBrightness(imageTensor, maxDelta) {
  // maxDeltaは0-1の範囲を想定 (入力テンソルの値域による)
  return tf.image.randomBrightness(imageTensor, maxDelta);
}

/**
 * 画像テンソルのコントラストをランダムに変更する
 * @param {tf.Tensor3D} imageTensor - [height, width, channels]の画像テンソル (0-1または0-255の範囲)
 * @param {number} lower - コントラストの最小スケール係数
 * @param {number} upper - コントラストの最大スケール係数
 * @returns {tf.Tensor3D} 変換後の画像テンソル
 */
function randomContrast(imageTensor, lower, upper) {
  return tf.image.randomContrast(imageTensor, lower, upper);
}

/**
 * 画像テンソルの色相をランダムに変更する (RGBテンソルが必要)
 * @param {tf.Tensor3D} imageTensor - [height, width, channels]の画像テンソル (0-1の範囲, RGB)
 * @param {number} maxDelta - 色相の最大変更量 (0-0.5)
 * @returns {tf.Tensor3D} 変換後の画像テンソル
 */
function randomHue(imageTensor, maxDelta) {
   // randomHueは入力がfloat型 (0-1) である必要があります
   // チャンネル数が3である必要があります
   if (imageTensor.shape[2] !== 3) {
       console.warn("randomHue requires a 3-channel image tensor.");
       return imageTensor;
   }
   const floatTensor = imageTensor.toFloat().div(255); // 0-255 -> 0-1に変換 (必要なら)
   return tf.image.randomHue(floatTensor, maxDelta);
}

/**
 * 画像テンソルの彩度をランダムに変更する (RGBテンソルが必要)
 * @param {tf.Tensor3D} imageTensor - [height, width, channels]の画像テンソル (0-1の範囲, RGB)
 * @param {number} lower - 彩度の最小スケール係数
 * @param {number} upper - 彩度の最大スケール係数
 * @returns {tf.Tensor3D} 変換後の画像テンソル
 */
function randomSaturation(imageTensor, lower, upper) {
    // randomSaturationは入力がfloat型 (0-1) である必要があります
    // チャンネル数が3である必要があります
    if (imageTensor.shape[2] !== 3) {
        console.warn("randomSaturation requires a 3-channel image tensor.");
        return imageTensor;
    }
    const floatTensor = imageTensor.toFloat().div(255); // 0-255 -> 0-1に変換 (必要なら)
    return tf.image.randomSaturation(floatTensor, lower, upper);
}

// 使用例 (0-255のuint8テンソルを仮定)
// const imageTensor = tf.browser.fromPixels(canvasElement); // [h, w, c], uint8 (0-255)
// const floatTensor = imageTensor.toFloat().div(255); // 0-1に正規化 (色の変換に必要)

// let augmentedTensor = randomBrightness(floatTensor, 0.2); // 明るさをランダムに変更
// augmentedTensor = randomContrast(augmentedTensor, 0.8, 1.2); // コントラストをランダムに変更
// augmentedTensor = randomHue(augmentedTensor, 0.1); // 色相をランダムに変更
// augmentedTensor = randomSaturation(augmentedTensor, 0.8, 1.2); // 彩度をランダムに変更

// augmentedTensor = augmentedTensor.mul(255).clipByValue(0, 255).toInt(); // 元の範囲に戻す (必要なら)

色の変換系の関数は、多くの場合入力テンソルが浮動小数点型(0-1の範囲)であることを想定しています。tf.browser.fromPixels などから取得したuint8(0-255)のテンソルを使用する場合は、toFloat().div(255) で変換する必要があります。

複数のデータ拡張を組み合わせる

実際のデータ拡張では、これらの操作を複数組み合わせて適用します。適用する操作の順序や確率を制御することで、より多様なデータセットを生成できます。

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

/**
 * 複数のデータ拡張操作を組み合わせる例
 * @param {tf.Tensor3D} imageTensor - [height, width, channels]の画像テンソル
 * @param {number} targetSize - 最終的な出力サイズ (正方形を想定)
 * @returns {tf.Tensor3D} 変換後の画像テンソル
 */
function applyAugmentations(imageTensor, targetSize) {
  let augmentedTensor = imageTensor;

  // 必ず最初にリサイズ (モデル入力サイズに合わせる)
  augmentedTensor = resizeImage(augmentedTensor, targetSize, targetSize);

  // ランダムに左右反転
  if (Math.random() < 0.5) {
    augmentedTensor = randomFlip(augmentedTensor, 'horizontal');
  }

  // ランダムに回転 (-10度から+10度)
  if (Math.random() < 0.7) { // 例えば70%の確率で適用
      augmentedTensor = randomRotate(augmentedTensor, 10);
  }

  // ランダムにクロップ&リサイズ (例えば、元の80%-100%の範囲でクロップし、targetSizeにリサイズ)
  // この操作は resizeImage の代わりに行うこともあります
  // if (Math.random() < 0.8) {
  //     augmentedTensor = randomCropAndResize(imageTensor, targetSize, targetSize, 0.8, 1.0);
  // } else {
  //     augmentedTensor = resizeImage(imageTensor, targetSize, targetSize);
  // }


  // 色の変換は浮動小数点テンソルで行うのが一般的
  let floatTensor = augmentedTensor.toFloat().div(255); // 0-1に正規化

  // ランダムに明るさ変更 (最大20%)
  if (Math.random() < 0.5) {
     floatTensor = randomBrightness(floatTensor, 0.2);
  }

  // ランダムにコントラスト変更 (80%-120%)
  if (Math.random() < 0.5) {
     floatTensor = randomContrast(floatTensor, 0.8, 1.2);
  }

  // HueとSaturationは3チャンネル画像のみに適用可能
  if (floatTensor.shape[2] === 3) {
      // ランダムに色相変更 (最大10%)
      if (Math.random() < 0.5) {
          floatTensor = randomHue(floatTensor, 0.1);
      }
      // ランダムに彩度変更 (80%-120%)
      if (Math.random() < 0.5) {
          floatTensor = randomSaturation(floatTensor, 0.8, 1.2);
      }
  }

  // 必要に応じて元のデータ型/範囲に戻す
  // augmentedTensor = floatTensor.mul(255).clipByValue(0, 255).toInt(); // 0-255に戻す場合

  // モデルの入力に合わせて最終的な前処理(例:MobileNetV2なら -1から1の範囲に正規化)
  // const normalizedTensor = floatTensor.mul(2).sub(1); // 0-1 -> -1 to 1

  // ここでは簡単のため、色の変換後のfloatTensor (0-1) を返す
  return floatTensor;
}

// 使用例
// assume originalImageTensor is loaded and is a tf.Tensor3D
// const augmentedImage = applyAugmentations(originalImageTensor, 224); // 224x224にリサイズ&データ拡張
// augmentedImage.print();

Python Keras ImageDataGenerator との関連性

Python Kerasの ImageDataGenerator は、これらのデータ拡張操作を一元的に管理し、ミニバッチごとにリアルタイムでデータ拡張を適用する機能を提供します。

from tensorflow.keras.preprocessing.image import ImageDataGenerator

datagen = ImageDataGenerator(
    rotation_range=15,
    width_shift_range=0.1,
    height_shift_range=0.1,
    shear_range=0.1,
    zoom_range=0.1,
    horizontal_flip=True,
    vertical_flip=False,
    brightness_range=[0.8, 1.2],
    # rescale=1./255, # 正規化
    # preprocessing_function=... # カスタム関数
)

# ディレクトリからの読み込みと拡張の適用
# train_generator = datagen.flow_from_directory(
#     'data/train',
#     target_size=(224, 224),
#     batch_size=32,
#     class_mode='categorical'
# )

# またはNumpy配列からの読み込み
# datagen.flow(x_train, y_train, batch_size=32)

TensorFlow.jsには ImageDataGenerator のような高レベルで統合されたクラスは標準では提供されていません(コミュニティライブラリには存在するかもしれません)。しかし、tf.image モジュールにある関数を組み合わせ、JavaScript/TypeScriptのコードでデータパイプラインを構築することで、同様の処理を実現できます。

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

/**
 * 複数画像にデータ拡張を適用し、バッチテンソルを作成する例
 * (これは概念コードであり、実際のファイル読み込みやデータ管理は別途必要です)
 * @param {Array<tf.Tensor3D>} imageTensors - [height, width, channels]の画像テンソルの配列
 * @param {number} targetSize - 最終的な出力サイズ
 * @returns {tf.Tensor4D} バッチテンソル [batch_size, targetSize, targetSize, channels]
 */
async function createAugmentedBatch(imageTensors, targetSize) {
  const augmentedTensors = [];
  for (const imgTensor of imageTensors) {
    // データ拡張を適用
    const augmentedImg = applyAugmentations(imgTensor, targetSize); // applyAugmentationsは0-1のfloatテンソルを返すと仮定

    // メモリリークを防ぐため、元のテンソルを解放 (必要に応じて)
    // imgTensor.dispose();

    augmentedTensors.push(augmentedImg);
  }

  // テンソルの配列をバッチテンソルにスタック
  const batchTensor = tf.stack(augmentedTensors);

  // 個別テンソルはスタック後に不要になるので解放
  augmentedTensors.forEach(t => t.dispose());

  return batchTensor;
}

// 使用例 (3枚の画像テンソルを準備したと仮定)
// const image1 = ...; // tf.Tensor3D
// const image2 = ...; // tf.Tensor3D
// const image3 = ...; // tf.Tensor3D
// const inputImages = [image1, image2, image3];
// const batchSize = inputImages.length;
// const targetImageSize = 224;

// const augmentedBatch = await createAugmentedBatch(inputImages, targetImageSize);
// console.log(augmentedBatch.shape); // [batchSize, targetImageSize, targetImageSize, channels]

// augmentedBatchを使用してモデルの学習や推論を行う
// model.fit(augmentedBatch, labelsBatch, ...);
// または model.predict(augmentedBatch);

// 処理完了後にテンソルを解放
// augmentedBatch.dispose();

Python Kerasの ImageDataGenerator がファイルの読み込みから拡張、バッチ化までを透過的に行ってくれるのに対し、TensorFlow.jsではファイル読み込み(Node.js/ブラウザで異なる)と画像デコード、データ拡張の関数適用、そしてテンソルのスタック(バッチ化)といったステップを明示的にコードで記述する必要があります。非同期処理を適切に扱うことも重要です。

実装上の注意点とパフォーマンス

まとめ

本記事では、TensorFlow.jsを使用して画像認識におけるデータ拡張を実装するための基本的なテクニックと具体的なコード例を紹介しました。tf.image モジュールには様々な画像操作関数が用意されており、これらを組み合わせることで、Python Kerasの ImageDataGenerator で実現できるようなデータ拡張パイプラインをJavaScript環境で構築することが可能です。

Pythonでの機械学習開発に慣れている方にとっては、ImageDataGenerator の手軽さとは異なる、より低レベルなAPIを使った構築作業になりますが、TensorFlow.jsの画像テンソル操作に慣れることで、ブラウザやNode.jsといった様々な環境での画像認識開発の幅が広がります。

今回紹介した以外にも、CutMixやMixupのような高度なデータ拡張手法や、TensorFlow.jsの tf.data.Dataset との連携など、さらに発展的なトピックがあります。これらの基礎として、まずは tf.image モジュールを使った基本的な画像操作とデータ拡張の実装を習得することが推奨されます。