今回は時系列データの予測で用いられるRNN(Recurrent Neural Network)をKerasを使わずにTensorFlow.jsだけでモデル化し、それを使って\(\sin\)曲線上の等間隔の3つのポイントから次のポイントを予測するプログラムを作成します。
RNNの定式化
通常、RNNは入力層、隠れ層、出力層の3層から構成され、\(\boldsymbol{x_{1}},\boldsymbol{x_{2}},\cdot\cdot\cdot,\boldsymbol{x_{n}}\)から\(\boldsymbol{x_{n+1}}\)を予測するRNNは下記の数式で表されます。\(\boldsymbol{x_{k}}\), \(\boldsymbol{h_{k}}\)はベクトル、\(f\)は活性化関数です。
sequence length:\(n\)
入力層:\(\boldsymbol{x_{1}},\boldsymbol{x_{2}},\cdot\cdot\cdot,\boldsymbol{x_{n}}\)
隠れ層:\(\boldsymbol{h_{1}},\boldsymbol{h_{2}},\cdot\cdot\cdot,\boldsymbol{h_{n}}\)
出力層:\(\boldsymbol{x_{n+1}}\)
今回は \(n=3\) のモデルを作成するので、次のようになります。
\[\boldsymbol{h_{1}}=f(\boldsymbol{x_{1}}W_{1}+\boldsymbol{b}_{1})\] \[\boldsymbol{h_{2}}=f(\boldsymbol{x_{2}}W_{1}+\boldsymbol{h_{1}}U_{1}+\boldsymbol{b}_{1})\] \[\boldsymbol{h_{3}}=f(\boldsymbol{x_{3}}W_{1}+\boldsymbol{h_{2}}U_{1}+\boldsymbol{b}_{1})\] \[\boldsymbol{x_{4}}=\boldsymbol{h_{3}}W_{2}+\boldsymbol{b}_{2}=f(\boldsymbol{x_{3}}W_{1}+f(\boldsymbol{x_{2}}W_{1}+f(\boldsymbol{x_{1}}W_{1}+\boldsymbol{b}_{1})U_{1}+\boldsymbol{b}_{1})U_{1}+\boldsymbol{b}_{1})W_{2}+\boldsymbol{b}_{2}\]
この式を使って\(\sin\)曲線上の\(x_{1}=\sin{0.01t},x_{2}=\sin{0.01(t+1)},x_{3}=\sin{0.01(t+2)}\)から\(x_{4}=\sin{0.01(t+3)}\)を予測するモデルを作成すると下記のようになります。活性化関数は\(\tanh\)を使っています。
rnn.mjs
import * as ph from "perf_hooks";
import * as tf from "@tensorflow/tfjs";
class NeuralNetwork {
constructor(units1, units2, units3) {
/* units1:第1層(入力層)のユニット数 */
/* units2:第2層(隠れ層)のユニット数 */
/* units3:第3層(出力層)のユニット数 */
this.w1 = tf.variable(tf.randomUniform([units1, units2], -1, 1));
this.u1 = tf.variable(tf.randomUniform([units2, units2], -1, 1));
this.b1 = tf.variable(tf.randomUniform([units2], -1, 1));
this.w2 = tf.variable(tf.randomUniform([units2, units3], -1, 1));
this.b2 = tf.variable(tf.randomUniform([units3], -1, 1));
}
model(x) {
const x1 = x.gather([0], 1);
const x2 = x.gather([1], 1);
const x3 = x.gather([2], 1);
const h1 = x1.matMul(this.w1).add(this.b1).tanh();
const h2 = x2.matMul(this.w1).add(h1.matMul(this.u1)).add(this.b1).tanh();
const h3 = x3.matMul(this.w1).add(h2.matMul(this.u1)).add(this.b1).tanh();
return h3.matMul(this.w2).add(this.b2);
}
loss(x, y) {
return tf.losses.meanSquaredError(this.model(x), y);
}
train(x, y, epochs) {
const optimizer = tf.train.adam(0.001, 0.9, 0.999, 0.00000001);
for (let epoch = 1; epoch <= epochs; epoch++) {
optimizer.minimize(() => this.loss(tf.tensor(x), tf.tensor(y)));
if (epoch % 1000 == 0) {
const loss = this.loss(tf.tensor(x), tf.tensor(y));
console.log(`${epoch} epoch: loss = ${loss.arraySync()}`);
}
}
}
predict(x) {
return this.model(tf.tensor([x])).arraySync()[0];
}
}
const neuralNetwork = new NeuralNetwork(1, 2, 1);
const n = 500;
const x_train = [];
const y_train = [];
for (let t = 0; t < n; t++) {
x_train.push([Math.sin(0.01 * (t + 0)), Math.sin(0.01 * (t + 1)), Math.sin(0.01 * (t + 2))]);
y_train.push([Math.sin(0.01 * (t + 3))]);
}
/* 学習 */
const start = ph.performance.now();
neuralNetwork.train(x_train, y_train, 50000);
const stop = ph.performance.now();
/* 検証 */
for (let t = 0; t < n; t++) {
const predictedValue = neuralNetwork.predict([Math.sin(0.01 * (t + 0.5)), Math.sin(0.01 * (t + 1.5)), Math.sin(0.01 * (t + 2.5))]);
const actualValue = Math.sin(0.01 * (t + 3.5));
console.log(`${Math.abs(predictedValue / actualValue - 1)}`);
}
console.log(`${(0.001 * (stop - start)).toFixed(3)}sec`);
まとめ
結果は省略しますが、units2 = 2、sequence length = 3 というコンパクトなモデルであるにも関わらず結構いい精度で予測できました。