noteのタイトル画像

多次元方程式を機械学習で解く(tensorflow.js)

目次
・さて、いよいよ機械学習の実装
・サンプルソース

●さて、いよいよ機械学習の実装

実際にプログラムを組んで機械学習をさせてみましょう

基本となるHTMLは次です。

<html>

<head>
  <!-- Load TensorFlow.js -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@0.13.0"> </script>

  <script>
    // ここにコードを書く
  </script>
</head>

<body>
</body>

</html>

scriptタグの中にコードを書いていきます。

まず、学習させる多次元方程式を定義します。
次のような方程式で試行することにします。
Xがインプットで、Yがアウトプットです。

Y = a × X ^ 3 + b × X ^ 2 + c × X + d

この方程式は係数a,b,cと定数dを持つ、Xを引数にとる3次の方程式です。
(学生の頃にお世話になりましたよね)

与える値Xと得られる値Yの組から、Xの入力に対してYに近い値になるa,b,c,dの組み合わせを機械学習で導き出してみましょう。

a,b,c,dを次の式で定義します。

const a = tf.variable(tf.scalar(Math.random()));
const b = tf.variable(tf.scalar(Math.random()));
const c = tf.variable(tf.scalar(Math.random()));
const d = tf.variable(tf.scalar(Math.random()));

最初は、なんでもいいので、Math.random関数で乱数をそれぞれに与えておきます。

実行してみると、a,b,c,dには

Tensor
    0.2709084451198578
Tensor
    0.42310482263565063
Tensor
    0.037558600306510925
Tensor
    0.4428211748600006

のようにバラバラな乱数が与えられているのがわかります。

次にオプティマイザを定義します。
学習によってa,b,c,dの各定数を最適化するための定義です。

// オプティマイザは学習レートは0.5として、tf.train.sgdから生成する。
// どうやら他にもいくつかのトレーニングオブジェクトが存在するらしいが、ここではSGDだけで済ます。
const learningRate = 0.5;
const optimizer = tf.train.sgd(learningRate);

学習レートというものが登場しました。
いまは深く考えずに例題通り0.5と設定しておきます。
(この値を増減させることで学習時間が変わるようです)

さて、次に実際のモデルを定義します。

Y = a × X ^ 3 + b × X ^ 2 + c × X + d

をモデル化すれば良いので、

// モデルを定義する
function predict(x) {
  // y = a * x ^ 3 + b * x ^ 2 + c * x + d
  // 上記の計算式をTFの関係式に落とし込むと次のようになる。
  // a,b,c,dは事前に決定した変数を当てている。
  return tf.tidy(() => {
    return a.mul(x.pow(tf.scalar(3, 'int32')))
      .add(b.mul(x.square()))
      .add(c.mul(x))
      .add(d);
  });
}

とします。
tf.tidyは前回も登場しましたが、メモリ管理の機構とだけ覚えておいていただければ良いです。
現段階ではまだ気にしないようにします。

mulメソッドは乗算で、addメソッドは足し算です。
powはべき乗を計算するものです。

つまり、a × X ^ 3 + b × X ^ 2 + c × X + d を表現しています。

次はモデルで計算した値が、与えられた数値に近いのか遠いのかを判断する仕組みが必要です。

ここで使うのは最小二乗法にしましょう。

最小二乗法の詳しい説明はこちらを参照ください。

このように差分の大きさを測る関数のことを「損失関数」と言うようです。
では次に損失関数を定義しましょう。

この関数は実際の値とモデルで予測した値の差をエラー値(誤差)として戻すだけのものです。
「差の二乗の平均」を取るには

(予測値 - 実測値) ^ 2 の全体平均

を計算すれば良いのですね。
プログラムは次のようになります。

function loss(prediction, labels) {
  const error = prediction.sub(labels).square().mean();
  return error;
}

第一引数と第二引数の差(sub)を二乗(square)して、平均(mean)を取っています。

さて、いよいよトレーニング(学習)関数を定義します。
単純にループで処理しましょう。

const numIterations = 100;
function train(xs, ys, numIterations) {
  for (let iter = 0; iter < numIterations; iter++) {
    optimizer.minimize(() => {
      const pred = predict(xs);
      return loss(pred, ys);
    });
  }
}

train関数は、第一引数と第二引数にXの値と、比較対象になるYの値を受け取り、ループ回数分 optimizerのminimizeメソッドを呼び出します。
そのコールバック関数内で

const pred = predict(xs);    // モデルでXからYを導く
return loss(pred, ys);       // 損失関数で差を確認

を実行します。
内部的には損失が一番少なくなるようなa,b,c,dの組み合わせを試しているようです。
そのために、a,b,c,dはtensorflowの組み込みメソッドから生成された変数である必要があるようです。
(そうでないと、tensorflowが学習する対象を特定できないのでしょう)

これで、入力するテストデータがあれば試験できます。

試験データはとりあえず、それらしいノイズを含んだデータにするために以下の関数で取得します。

// テストデータがないと話にならないので、テストデータを作る。
// サンプルにあったテストデータ作成関数だ。
// numPointsでデータ個数を指定
// coeffで「a,b,c,d」の定数を与えている。
// sigmaはランダムノイズを与えるときの定数
function generateData(numPoints, coeff, sigma = 0.04) {
  return tf.tidy(() => {
    const [a, b, c, d] = [
      tf.scalar(coeff.a), tf.scalar(coeff.b), tf.scalar(coeff.c),
      tf.scalar(coeff.d)
    ];

    const xs = tf.randomUniform([numPoints], -1, 1);

    // Generate polynomial data
    const ys = a.mul(xs.pow( tf.scalar(3, 'int32')))
      .add(b.mul(xs.square()))
      .add(c.mul(xs))
      .add(d)
      // Add random noise to the generated data
      // to make the problem a bit more interesting
      .add(tf.randomNormal([numPoints], 0, sigma));

    return {
      xs,
      ys,
    };
  })
}

試験データを作るときは次のようにします。

// 試しにお試しデータを作ってみる。
// 定数の設定
const eff = {
  a: -3,
  b: 2,
  c: -4,
  d: 7
};

//試験データ作成
// データは20組作成
const tData = generateData(20, eff);

上記のtDataに今回使う3次元方程式+ノイズの値が含まれています。
(上記の例は -3 × X ^ 3 + 2 × X ^ 2 - 4 × X + 7 という方程式です)

ここまでできれば後は学習させるだけです。

// 学習
train(tData.xs, tData.ys, numIterations);    // 100回ループ

学習後のa,b,c,dの値はどうなったでしょうか。

教示データの作成に使用したa,b,c,dは

a = -3, b = 2, c = -4, d = 7

でした。
学習後のaからdの値は次のようになっていました。

a =     -2.8895609378814697

b =     1.9329293966293335

c =     -4.106947422027588

d =     7.002674579620361

かなり近い値になっているのがわかります。

このように、モデルに対して値を与えて学習させることで、不明だった定数a,b,c,dの近似値を得ることができました。

単純な例ではありますが、tensorflowが意外と使えるなという感触を得る事ができました。

今後、トレードデータ等を餌にして、色々な学習にもチャレンジしてみたいと思います。
うまくすればbotのデータ解析部分に活用できるかもしれません。

今後にご期待いただければ、スキボタンをぽちっとお願いいたします。

●サンプルソース

参考までに今回使ったサンプルソース全体を貼っておきます。
chromeなどのブラウザで実行し、デバッグコンソールで内容を確認してみてください。

<html>

<head>
  <!-- Load TensorFlow.js -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@0.13.0"> </script>

  <script>
    console.log('トレーニングの第1ステップ:曲線を合成データにフィットさせる');
    // トレーニングの第1ステップ:曲線を合成データにフィットさせる
    // https://github.com/tensorflow/tfjs-examples

    // Y = a × X ^ 3 + b × X ^ 2 + c × X + d
    // という3次の方程式を考える。
    // 係数 a, b, c, と定数dが不定だということでとりあえずランダム値を与えておく。
    const a = tf.variable(tf.scalar(Math.random()));
    const b = tf.variable(tf.scalar(Math.random()));
    const c = tf.variable(tf.scalar(Math.random()));
    const d = tf.variable(tf.scalar(Math.random()));

    console.log('a-dの値(初期値)');
    // print
    a.print();
    b.print();
    c.print();
    d.print();
    // 何かの値が4つ出力されることがわかる。

    // オプティマイザは学習レートは0.5として、tf.train.sgdから生成する。
    // どうやら他にもいくつかのトレーニングオブジェクトが存在するらしいが、ここではSGDだけで済ます。
    const learningRate = 0.5;
    const optimizer = tf.train.sgd(learningRate);

    // モデルを定義する
    function predict(x) {
      // y = a * x ^ 3 + b * x ^ 2 + c * x + d
      // 上記の計算式をTFの関係式に落とし込むと次のようになる。
      // a,b,c,dは事前に決定した変数を当てている。
      return tf.tidy(() => {
        return a.mul(x.pow(tf.scalar(3, 'int32')))
          .add(b.mul(x.square()))
          .add(c.mul(x))
          .add(d);
      });
    }
    // モデルの動作を確認してみる。
    // 例として値に2を与えたら
    console.log('損失関数の出力の例(Input:2)');
    const r0 = predict(tf.scalar(2));
    r0.print();  // 与えたXにて計算されたYの値が出力されることがわかる。

    // さて、次に損失関数を定義しましょう。
    // この関数は実際の値とモデルで予測した値の差をエラー値(誤差)として戻すだけのものです。
    // よく最小二乗法とかで利用される「差の二乗の平均」を取るには
    //   (予測値 - 実測値) ^ 2 の全体平均
    // を prediction.sub(labels).square().mean() で計算する。
    function loss(prediction, labels) {
      const error = prediction.sub(labels).square().mean();
      return error;
    }

    // ここで学習ロジックを作る。
    // 100回のループで、オプティマイザのminimizeメソッドを呼び出して、損失関数の戻りを戻している。
    // xs,ysは不変なので、ここではa,b,c,dの値が変更されているということになるのだろう。
    const numIterations = 100;
    function train(xs, ys, numIterations) {
      for (let iter = 0; iter < numIterations; iter++) {
        optimizer.minimize(() => {
          const pred = predict(xs);
          return loss(pred, ys);
        });
      }
    }

    // テストデータがないと話にならないので、テストデータを作る。
    // サンプルにあったテストデータ作成関数だ。
    // numPointsでデータ個数を指定
    // coeffで「a,b,c,d」の定数を与えている。
    // sigmaはランダムノイズを与えるときの定数?なのかな?
    function generateData(numPoints, coeff, sigma = 0.04) {
      return tf.tidy(() => {
        const [a, b, c, d] = [
          tf.scalar(coeff.a), tf.scalar(coeff.b), tf.scalar(coeff.c),
          tf.scalar(coeff.d)
        ];

        const xs = tf.randomUniform([numPoints], -1, 1);

        const ys = a.mul(xs.pow( tf.scalar(3, 'int32')))
          .add(b.mul(xs.square()))
          .add(c.mul(xs))
          .add(d)
          .add(tf.randomNormal([numPoints], 0, sigma)); // ノイズ付加
        return {
          xs,
          ys,
        };
      })
    }

    // a = -3, b = 2, c = -4, d = 7 を定義する。
    const eff = {
      a: -3,
      b: 2,
      c: -4,
      d: 7
    };
    console.log('与えた定数');
    console.log(`a: ${eff.a}`);
    console.log(`b: ${eff.b}`);
    console.log(`c: ${eff.c}`);
    console.log(`d: ${eff.d}`);

    //試験データ作成
    const r1 = generateData(20, eff);
    console.log('試験データ:x');
    r1.xs.print();
    console.log('試験データ:y');
    r1.ys.print();

    // 学習
    console.log('学習開始');
    train(r1.xs, r1.ys, numIterations);

    console.log('学習後');
    // 学習の結果はどうか
    a.print();  // -3に近い値
    b.print();  // 2に近い値
    c.print();  // -4に近い値
    d.print();  // 7に近い値

  </script>
</head>

<body>
</body>

</html>


ソフトウェア・エンジニアを40年以上やってます。 「Botを作りたいけど敷居が高い」と思われている方にも「わかる」「できる」を感じてもらえるように頑張ります。 よろしくお願い致します。