TensorFlow.js - 2D データから予測を行う

この Codelab では、複数の自動車について記述された数値データから予測を行うモデルをトレーニングします。

この演習では、さまざまな種類のモデルをトレーニングする一般的な手順を説明しますが、ここでは小さなデータセットとシンプルな(浅い)モデルを使用します。主な目的は、TensorFlow.js を使用したトレーニング モデルに関する基本的な用語、コンセプト、構文の理解を深め、さらに調査と学習を進めるための足掛かりを提供することです。

これから行う作業は、連続した数値を予測するモデルをトレーニングするため、回帰タスクとも呼ばれます。ここでは、多くの入力例を正しい出力とともに使用してモデルをトレーニングします。これは、教師あり学習と呼ばれます。

作業内容

ブラウザで TensorFlow.js を使用してモデルをトレーニングするウェブページを作成します。モデルは、与えられた自動車の「馬力」から、その自動車の「燃費」(MPG)を予測するように学習します。

以下に手順を示します。

  • データを読み込んでトレーニング用に準備します。
  • モデルのアーキテクチャを定義します。
  • モデルをトレーニングします。さらに、トレーニングしながらパフォーマンスをモニタリングします。
  • 予測を行うことで、トレーニングされたモデルを評価します。

ラボの内容

  • 機械学習のデータを準備するベスト プラクティス(シャッフルと正規化など)。
  • tf.layers API を使用してモデルを作成するための TensorFlow.js 構文。
  • tfjs-vis ライブラリを使用してブラウザ内トレーニングをモニタリングする方法。

必要なもの

HTML ページを作成して JavaScript を追加する

96914ff65fc3b74c.png 次の html ファイルにその下に記述されているコードをコピーします。

index.html

<!DOCTYPE html>
<html>
<head>
  <title>TensorFlow.js Tutorial</title>

  <!-- Import TensorFlow.js -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.0.0/dist/tf.min.js"></script>
  <!-- Import tfjs-vis -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.0.2/dist/tfjs-vis.umd.min.js"></script>

  <!-- Import the main script file -->
  <script src="script.js"></script>

</head>

<body>
</body>
</html>

コードの JavaScript ファイルを作成する

  1. 前述の HTML ファイルと同じフォルダに script.js というファイルを作成し、次のコードを記述します。
console.log('Hello TensorFlow');

テストする

この時点で、HTML ファイルと JavaScript ファイルが作成されていますので、それをテストします。ブラウザで index.html ファイルを開き、次に DevTools コンソールを開きます。

すべてが正常に機能している場合は、DevTools コンソールで 2 つのグローバル変数が作成され、使用可能になっています。

  • tf は、TensorFlow.js ライブラリを指します。
  • tfvis は、tfjs-vis ライブラリを指しますす。

ブラウザのデベロッパー ツールを開くと、コンソール出力で「Hello TensorFlow」というメッセージを確認できます。確認できたら、次の手順に進みます。

最初のステップとして、モデルのトレーニングに使用するデータを読み込み、フォーマットして可視化します。

このために用意された JSON ファイルから、「cars」データセットを読み込みます。このデータセットには、各車種に関する多くの特徴が含まれています。このチュートリアルでは、馬力と、1 ガロンあたりの走行マイル(MPG)のデータのみを抽出します。

96914ff65fc3b74c.png 次のコードを

script.js ファイルに追加します。

/**
 * Get the car data reduced to just the variables we are interested
 * and cleaned of missing data.
 */
async function getData() {
  const carsDataResponse = await fetch('https://storage.googleapis.com/tfjs-tutorials/carsData.json');
  const carsData = await carsDataResponse.json();
  const cleaned = carsData.map(car => ({
    mpg: car.Miles_per_Gallon,
    horsepower: car.Horsepower,
  }))
  .filter(car => (car.mpg != null && car.horsepower != null));

  return cleaned;
}

これにより、1 ガロンあたりの走行マイルや馬力が示されていないエントリは、すべて削除されます。また、このデータを散布図にプロットして、どのように表示されるか確認しましょう。

96914ff65fc3b74c.png 次のコードを

script.js ファイルの末尾に追加します。

async function run() {
  // Load and plot the original input data that we are going to train on.
  const data = await getData();
  const values = data.map(d => ({
    x: d.horsepower,
    y: d.mpg,
  }));

  tfvis.render.scatterplot(
    {name: 'Horsepower v MPG'},
    {values},
    {
      xLabel: 'Horsepower',
      yLabel: 'MPG',
      height: 300
    }
  );

  // More code will be added below
}

document.addEventListener('DOMContentLoaded', run);

ページを更新すると、画面左のパネルにデータの散布図が表示されます。次のような図です。

cf44e823106c758e.png

このパネルは、バイザーと呼ばれ、tfjs-vis によって表示されています。簡単に可視化できる領域を提供します。

一般に、データを扱うときは、データを目視で確認する方法を見つけ、必要に応じてきれいにすることをおすすめします。この場合は、必須項目が欠落しているエントリを、ある程度 carsData から削除する必要がありました。データを可視化することで、モデルが学習できる構造がデータにあるかどうかを判断できます。

上図からは、馬力と MPG に負の相関関係があることがわかります。つまり、馬力が上がるにつれて、自動車の 1 ガロンあたりの走行距離は一般に短くなります。

タスクを概念化する

この時点で、入力データは次のようになっています。

...
{
  "mpg":15,
  "horsepower":165,
},
{
  "mpg":18,
  "horsepower":150,
},
{
  "mpg":16,
  "horsepower":150,
},
...

ここでの目標は、モデルをトレーニングして、1 つの数値(馬力) から、1 つの数値 (1 ガロンあたりの走行マイル)を予測するように学習させることです。この 1 対 1 のマッピングを覚えておいてください。これは、次のセクションで重要になります。

こうしたサンプル(馬力と MPG)をニューラル ネットワークに与えると、そこから与えられた馬力に対する MPG を予測する数式(または関数)を学習します。こうした正解があるサンプルからの学習は、教師あり学習と呼ばれます。

このセクションでは、モデル アーキテクチャを記述するコードを作成します。モデル アーキテクチャでは、モデルの実行中に呼び出す関数や、答えの計算にモデルが使用するアルゴリズムを定義できます。

機械学習(ML)モデルとは、入力を受け取って出力を生成するアルゴリズムのことです。ニューラル ネットワークを使用する場合、そのアルゴリズムは、「重み」付けされたニューロンの層(レイヤ)が集まったもので、それが出力を決定します。トレーニング プロセスによって、これらの重みに最適な値を学習します。

96914ff65fc3b74c.png 次の関数を

script.js ファイルに追加して、モデル アーキテクチャを定義します。

function createModel() {
  // Create a sequential model
  const model = tf.sequential();

  // Add a single input layer
  model.add(tf.layers.dense({inputShape: [1], units: 1, useBias: true}));

  // Add an output layer
  model.add(tf.layers.dense({units: 1, useBias: true}));

  return model;
}

これは、tensorflow.js で定義できる最もシンプルなモデルの 1 つです。各行を細かく見てみましょう。

モデルをインスタンス化する

const model = tf.sequential();

この行により、tf.Model オブジェクトがインスタンス化されます。入力値は出力までまっすぐ流れていくため、このモデルは sequential です。他の種類のモデルには、分岐や、複数の入力と出力を持つものもありますが、多くの場合、モデルはシーケンシャルになります。また、シーケンシャル モデルでは、API の使用も他より簡単です。

レイヤを追加する

model.add(tf.layers.dense({inputShape: [1], units: 1, useBias: true}));

ここでは、入力レイヤをネットワークに追加し、隠れユニットが 1 つある dense レイヤに自動的に接続されます。dense レイヤとは、レイヤの種類の 1 つで、入力値と行列(重み)を乗算し、その結果に数値(バイアス)を加算します。これはネットワークの第 1 レイヤになるため、inputShape を定義する必要があります。入力として 1 つの数(特定の自動車の馬力)があるため、inputShape[1] になります。

units には、レイヤ内の重みマトリックスのサイズを設定します。これを 1 に設定すると、データの入力特徴量ごとに 1 の重みがあることになります。

model.add(tf.layers.dense({units: 1}));

上のコードは、出力レイヤを作成します。出力を 1 つの数値にするため、units1 に設定します。

インスタンスを作成する

96914ff65fc3b74c.png 次のコードを

前に定義した run 関数に追加します。

// Create the model
const model = createModel();
tfvis.show.modelSummary({name: 'Model Summary'}, model);

この記述により、モデルのインスタンスが作成され、ウェブページ上にレイヤ全体の概要が表示されます。

機械学習モデルのトレーニングを実用的なものにする TensorFlow.js のパフォーマンス上のメリットを得るには、データをテンソルに変換する必要があります。また、ベスト プラクティスとなっているデータのシャッフル正規化など、さまざまな変換も行います。

96914ff65fc3b74c.png 次のコードを

script.js ファイルに追加します。

/**
 * Convert the input data to tensors that we can use for machine
 * learning. We will also do the important best practices of _shuffling_
 * the data and _normalizing_ the data
 * MPG on the y-axis.
 */
function convertToTensor(data) {
  // Wrapping these calculations in a tidy will dispose any
  // intermediate tensors.

  return tf.tidy(() => {
    // Step 1. Shuffle the data
    tf.util.shuffle(data);

    // Step 2. Convert data to Tensor
    const inputs = data.map(d => d.horsepower)
    const labels = data.map(d => d.mpg);

    const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
    const labelTensor = tf.tensor2d(labels, [labels.length, 1]);

    //Step 3. Normalize the data to the range 0 - 1 using min-max scaling
    const inputMax = inputTensor.max();
    const inputMin = inputTensor.min();
    const labelMax = labelTensor.max();
    const labelMin = labelTensor.min();

    const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin));
    const normalizedLabels = labelTensor.sub(labelMin).div(labelMax.sub(labelMin));

    return {
      inputs: normalizedInputs,
      labels: normalizedLabels,
      // Return the min/max bounds so we can use them later.
      inputMax,
      inputMin,
      labelMax,
      labelMin,
    }
  });
}

何が行われているか詳しく見てみましょう。

データをシャッフルする

// Step 1. Shuffle the data
tf.util.shuffle(data);

この行では、トレーニング アルゴリズムに与えるサンプルの順序をランダムに並べ替えます。通常、トレーニングの間、データセットは小さなサブセット(バッチ)に分解され、それを使用してモデルがトレーニングされるため、シャッフルすることが重要になります。シャッフルにより、各バッチでは、データ分布全体からさまざまなデータを持つことができます。また、それにより、モデルが次のように強化されます。

  • データの供給順序だけに関係する情報を学習しない。
  • サブグループの構造から影響を受けない(たとえば、トレーニングの前半に大きな馬力の自動車のみが表示される場合、データセットの残りの部分には当てはまらない関係が学習される可能性があります)。

テンソルに変換する

// Step 2. Convert data to Tensor
const inputs = data.map(d => d.horsepower)
const labels = data.map(d => d.mpg);

const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
const labelTensor = tf.tensor2d(labels, [labels.length, 1]);

ここでは、2 つの配列を作成します。1 つは入力サンプル(馬力エントリ)用、もう 1 つは真の出力値(機械学習のラベル)用です。

次に、各配列データを 2D テンソルに変換します。テンソルの形状は [num_examples, num_features_per_example] になります。inputs.length のサンプルと、それぞれのサンプル1 つの入力特徴(馬力)があります。

データを正規化する

//Step 3. Normalize the data to the range 0 - 1 using min-max scaling
const inputMax = inputTensor.max();
const inputMin = inputTensor.min();
const labelMax = labelTensor.max();
const labelMin = labelTensor.min();

const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin));
const normalizedLabels = labelTensor.sub(labelMin).div(labelMax.sub(labelMin));

次に、機械学習トレーニングに関するもう 1 つのベスト プラクティスを使用します。データの正規化です。この行では、Min-Max スケーリングを使用して、データを数値範囲 0-1 に正規化します。tensorflow.js で構築する多くの機械学習モデルの内部要素は、それほど大きくない数字を扱うように設計されているため、正規化することが重要になります。データの正規化でよく使われる範囲は、0 to 1-1 to 1 です。ある程度の妥当な範囲にデータを正規化する習慣が身に付くと、モデルのトレーニングがさらにうまくできるようになります。

データと正規化の境界を返す

return {
  inputs: normalizedInputs,
  labels: normalizedLabels,
  // Return the min/max bounds so we can use them later.
  inputMax,
  inputMin,
  labelMax,
  labelMin,
}

トレーニング中は、正規化した値を元のまま保持します。そうすることにより、出力を非正規化して元のスケールに戻し、将来の入力データを同じ方法で正規化できます。

モデル インスタンスを作成して、データをテンソルとして表現したことで、トレーニング プロセスを開始するための準備はすべて整いました。

96914ff65fc3b74c.png 次の関数を

script.js ファイルにコピーします。

async function trainModel(model, inputs, labels) {
  // Prepare the model for training.
  model.compile({
    optimizer: tf.train.adam(),
    loss: tf.losses.meanSquaredError,
    metrics: ['mse'],
  });

  const batchSize = 32;
  const epochs = 50;

  return await model.fit(inputs, labels, {
    batchSize,
    epochs,
    shuffle: true,
    callbacks: tfvis.show.fitCallbacks(
      { name: 'Training Performance' },
      ['loss', 'mse'],
      { height: 200, callbacks: ['onEpochEnd'] }
    )
  });
}

これを詳しく確認してみましょう。

トレーニングの準備

// Prepare the model for training.
model.compile({
  optimizer: tf.train.adam(),
  loss: tf.losses.meanSquaredError,
  metrics: ['mse'],
});

モデルは、トレーニングする前に「コンパイル」する必要があります。それを行うには、重要なものをいくつか指定する必要があります。

  • optimizer: これは、例に示すようにモデルの更新を管理するアルゴリズムです。TensorFlow.js にはさまざまなオプティマイザーが用意されています。ここでは、実際に非常に有効であり、構成も必要としない adam オプティマイザーを選択しています。
  • loss: 表示される各バッチ(データ サブセット)の学習状況をモデルに伝える関数です。ここでは、meanSquaredError を使用して、モデルによる予測と実際の値を比較します。
const batchSize = 32;
const epochs = 50;

次に、batchSize と epochs の数について説明します。

  • batchSize は、トレーニングの各イテレーションでモデルに通されるデータ サブセットのサイズを意味します。一般的なバッチサイズは、32~512 の範囲です。すべての問題に最適なバッチサイズというわけではありませんが、さまざまなバッチサイズに関する数学的な話題については、このチュートリアルでは扱いません。
  • epochs は、提供されたデータセット全体がモデルを通る回数です。ここでは、データセットを 50 回繰り返します。

トレーニング ループを開始する

return await model.fit(inputs, labels, {
  batchSize,
  epochs,
  callbacks: tfvis.show.fitCallbacks(
    { name: 'Training Performance' },
    ['loss', 'mse'],
    { height: 200, callbacks: ['onEpochEnd'] }
  )
});

model.fit は、トレーニング ループを開始するために呼び出す関数です。これは非同期関数であるため、この関数が提供する Promise オブジェクトを返します。これにより、呼び出し元はトレーニングが完了したかどうかを判断できます。

トレーニングの進行状況をモニタリングするために、いくつかのコールバックを model.fit に渡します。tfvis.show.fitCallbacks を使用して、前に指定した「loss」と「mse」の指標のチャートを描く関数を生成します。

すべてをまとめる

次に、定義した関数を run 関数から呼び出す必要があります。

96914ff65fc3b74c.png 次のコードを

run 関数の末尾に追加します。

// Convert the data to a form we can use for training.
const tensorData = convertToTensor(data);
const {inputs, labels} = tensorData;

// Train the model
await trainModel(model, inputs, labels);
console.log('Done Training');

ページを更新すると、数秒後に次のグラフが更新されます。

c6d3214d6e8c3752.png

これらは、前に作成したコールバックによって作成されます。各エポックの最後で、データセット全体にわたって平均した loss と mse が表示されます。

モデルをトレーニングすると、loss は小さくなることが望ましい形です。ここでは、指標が誤差の程度を示すため、それも減少することが求められます。

以上でモデルはトレーニングされました。次に予測を行います。馬力の数値が小さいものから大きいものまで、一定の範囲に対する予測を確認することでモデルを評価します。

96914ff65fc3b74c.png 次の関数を script.js ファイルに追加します。

function testModel(model, inputData, normalizationData) {
  const {inputMax, inputMin, labelMin, labelMax} = normalizationData;

  // Generate predictions for a uniform range of numbers between 0 and 1;
  // We un-normalize the data by doing the inverse of the min-max scaling
  // that we did earlier.
  const [xs, preds] = tf.tidy(() => {

    const xs = tf.linspace(0, 1, 100);
    const preds = model.predict(xs.reshape([100, 1]));

    const unNormXs = xs
      .mul(inputMax.sub(inputMin))
      .add(inputMin);

    const unNormPreds = preds
      .mul(labelMax.sub(labelMin))
      .add(labelMin);

    // Un-normalize the data
    return [unNormXs.dataSync(), unNormPreds.dataSync()];
  });

  const predictedPoints = Array.from(xs).map((val, i) => {
    return {x: val, y: preds[i]}
  });

  const originalPoints = inputData.map(d => ({
    x: d.horsepower, y: d.mpg,
  }));

  tfvis.render.scatterplot(
    {name: 'Model Predictions vs Original Data'},
    {values: [originalPoints, predictedPoints], series: ['original', 'predicted']},
    {
      xLabel: 'Horsepower',
      yLabel: 'MPG',
      height: 300
    }
  );
}

前述の関数で注目すべき点を、いくつか説明します。

const xs = tf.linspace(0, 1, 100);
const preds = model.predict(xs.reshape([100, 1]));

モデルに与える新しい 100 個の「サンプル」を生成します。model.predict が、こうしたサンプルをモデルに取り込みます。なお、このサンプルは、トレーニングと同様の形状([num_examples, num_features_per_example])にする必要があります。

// Un-normalize the data
const unNormXs = xs
  .mul(inputMax.sub(inputMin))
  .add(inputMin);

const unNormPreds = preds
  .mul(labelMax.sub(labelMin))
  .add(labelMin);

データを 0~1 ではなく元の範囲に戻すには、正規化中に計算した値を使用しますが、演算は逆にします。

return [unNormXs.dataSync(), unNormPreds.dataSync()];

.dataSync() は、テンソルに保存された値の typedarray を取得するために使用できるメソッドです。これを使用すると、通常の JavaScript でこれらの値を処理できます。これは、一般的に好まれる .data() メソッドの同期バージョンです。

最後に、tfjs-vis を使用して、元のデータとモデルからの予測をグラフに描画します。

96914ff65fc3b74c.png 次のコードを

run 関数に追加します。

// Make some predictions using the model and compare them to the
// original data
testModel(model, data, tensorData);

モデルのトレーニング完了後、ページを更新すると、次のような表示が確認できます。

fe610ff34708d4a.png

これで完了です。シンプルな機械学習モデルをトレーニングしました。現在は、入力データのトレンドに線を合わせようする、線形回帰と呼ばれる処理を行います。

機械学習モデルをトレーニングする手順は、次のとおりです。

タスクの明確化:

  • 回帰問題なのか、それとも分類するものか。
  • 教師あり学習で可能なのか、それとも教師なし学習で可能なのか。
  • 入力データの形状はどうなるか。出力データはどのような形になるのか。

データの準備:

  • データをきれいにし、可能であればパターンを手動で検査する。
  • トレーニングに使用する前にデータをシャッフルする。
  • データをニューラル ネットワークに適した範囲内に正規化する。通常、数値データには、0~1 か、-1~1 の範囲が適しています。
  • データをテンソルに変換する。

モデルの構築と実行:

  • tf.sequential または tf.model でモデルを定義し、tf.layers.* を使用してレイヤを追加する。
  • オプティマイザー(通常は adam が適しています)、バッチサイズ、エポック数などのパラメータを選択する。
  • 問題に適した損失関数を選択し、精度指標に基づいて進行状況を評価する。meanSquaredError は回帰問題の一般的な損失関数です。
  • トレーニングをモニタリングして、損失が減少しているかどうかを確認する。

モデルの評価

  • トレーニング中にモニタリングできるモデルの評価指標を選択する。トレーニングが終了したら、テスト予測を行って予測品質を把握します。
  • エポックの数を変更してみる。グラフが平坦になるまでに必要なエポック数はいくつでしょうか。
  • 隠れレイヤのユニットを増やしてみる。
  • 最初に追加した隠れレイヤと最終的な出力レイヤの間に、隠れレイヤを追加してみる。こうした追加レイヤのコードは、次のようになります。
model.add(tf.layers.dense({units: 50, activation: 'sigmoid'}));

隠れレイヤに関する最も重要な新しい点は、非線形アクティベーション関数(ここでは sigmoid アクティベーション)が導入されたことです。アクティベーション関数についての詳細は、こちらの記事をご覧ください。

そのモデルで、次の画像のような出力が可能かどうかを確認します。

a21c5e6537cf81d.png