モバイル&ワイヤレスブロードバンドでインターネットへ
TensorFlow.js (tfjs) は、ブラウザと Node.js 内で機械学習モデルの訓練とデプロイを行うための JavaScript ライブラリです。
https://www.tensorflow.org/
これまで、QuickEdit Text Editor Pro で TensorFlow.js の CODE SAMPLE FOR SCRIPT TAG SETUP をプレビュー や tfjs-vis で sin 波を。スマホのテキストエディタで。、tfjs で Early Stopping。スマホのテキストエディタで。 の記事で tfjs についてみてきましたが、今回はひとつの目標としていた tfjs で sin 波の機械学習を試します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>サンプル</title>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"> </script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis"></script>
<script>
var _debug;
var _dct = 0;
function _debugStart(){
_debug = document.getElementById("DEBUG");
_debug.textContent = "";
_debugHTML("DEBUG START");
}
function _debugHTML(_deHTML){
_debug.insertAdjacentHTML("afterbegin", "<div><em>" + ++_dct + " : </em>" + _deHTML + "<div>");
}
console.log = function(_dobj){
_debugHTML(_dobj);
};
</script>
</head>
<body>
<div id="trainStatus"></div>
<div id="lossChart"></div>
<div id="predictStatus"></div>
<div id="sinChart"></div>
<div id="DEBUG"></div>
<script>
_debugStart();
_debugHTML("tfjs sin START");
function tensor_sin(T=100, ampl=0.05){
return tf.tidy(() => {
const L = 2 * T + 1;
const noise = tf.randomUniform([L], -1.0, 1.0);
const an = tf.mul(noise, tf.scalar(ampl));
const x = tf.range(0, L);
const xp = tf.mul(x, tf.scalar(2.0 * Math.PI));
const sx = tf.sin(tf.div(xp, tf.scalar(T)));
const sn = tf.add(sx, an);
return sn;
});
}
class EarlyStopping{
constructor(patience=0, verbose=0){
this._step = 0;
this._loss = Number.POSITIVE_INFINITY;
this.patience = patience;
this.verbose = verbose;
}
validate(loss){
if(this._loss < loss){
this._step += 1;
if(this._step > this.patience){
if(this.verbose){
console.log("early stopping");
}
return true;
}
}else{
this._step = 0;
this._loss = loss;
}
return false;
}
}
// 即時関数 Immediately Invoked Function Expression ( IIFE )
(async () => {
// データの生成
const T = 100;
const f = tensor_sin(T, 0.05);
const v = tensor_sin(T, 0.05);
const n = tensor_sin(T, 0.0);
// 時系列データの作成
const length_of_sequences = 2 * T;
const maxlen = 25; // ひとつの時系列データの長さ
// const farray = f.dataSync();
const varray = v.dataSync();
// const narray = n.dataSync();
const fdata = [];
const vdata = [];
const target = [];
for(i=0; i < length_of_sequences - maxlen + 1; i++){
fdata.push(f.slice(i, maxlen));
vdata.push(v.slice(i, maxlen));
target.push(n.slice(i + maxlen, 1));
}
const N_train = length_of_sequences - maxlen + 1;
const N_validation = length_of_sequences - maxlen + 1;
const X_train = tf.stack(fdata).reshape([N_train, maxlen, 1]);
const X_validation = tf.stack(vdata).reshape([N_validation, maxlen, 1]);
const Y_train = tf.stack(target).reshape([N_train, 1]);
const Y_validation = tf.stack(target).reshape([N_validation, 1]);
// モデル設定
const n_in = 1;
const n_hidden = 20;
const n_out = 1;
const model = tf.sequential();
model.add(tf.layers.simpleRNN({
units: n_hidden,
kernelInitializer: "randomNormal",
inputShape: [maxlen, n_in]
}));
model.add(tf.layers.dense({
units: n_out,
kernelInitializer: "randomNormal"
}));
model.add(tf.layers.activation({
activation: "linear"
}));
// compile
model.compile({
loss: "meanSquaredError",
optimizer: "adam",
metrics: ["accuracy"]
});
// fit & predict
const iterations = 100;
const epochs = 1;
const batchSize = 20;
const lossValues = [[], []];
const accuracyValues = [[], []];
let epochcnt = 0;
const early_stopping = new EarlyStopping(10, 1);
for (let i = 0; i < iterations; ++i) {
const beginMs = performance.now();
const history = await model.fit(X_train, Y_train, {
epochs: epochs,
batchSize: batchSize,
validationSplit: 0.1,
validationData: [X_validation, Y_validation],
callbacks: {
onEpochBegin: async (epoch, logs) => {
epochcnt = epoch;
},
onBatchEnd: async (batch, logs) => {
let elapsedMs = performance.now() - beginMs;
let modelFitTime = elapsedMs / 1000;
document.getElementById("trainStatus").textContent = `Iteration ${i+1} of ${iterations} (Epoch ${epochcnt+1}, Batch ${batch}): Fit time ${modelFitTime.toFixed(3)}s`;
},
onEpochEnd: async (epoch, logs) => {
// Early Stopping チェック
if(early_stopping.validate(logs["val_loss"])){
model.stopTraining = true;
}
}
}
});
let elapsedMs = performance.now() - beginMs;
let modelFitTime = elapsedMs / 1000;
document.getElementById("trainStatus").textContent = `Iteration ${i+1} of ${iterations}: Fit time ${modelFitTime.toFixed(3)}s`;
lossValues[0].push({"x": i, "y": history.history.loss[0]});
lossValues[1].push({"x": i, "y": history.history.val_loss[0]});
accuracyValues[0].push({"x": i, "y": history.history.acc[0]});
accuracyValues[1].push({"x": i, "y": history.history.val_acc[0]});
const lossContainer = document.getElementById("lossChart");
tfvis.render.linechart(
lossContainer,
{values: lossValues, series: ["train", "validation"]},
{
width: 300,
height: 300,
xLabel: "Iteration",
yLabel: "loss"
});
let predicted = [];
const predictMs = performance.now();
tf.tidy(() => {
let y = v.slice(0, maxlen).reshape([maxlen]);
for(let ix=0; ix < T - maxlen + 1; ix++){
const Z = v.slice(ix, maxlen).reshape([1, maxlen, 1]);
const Y = model.predict(Z);
y = y.concat(Y.slice(0, 1).reshape([1]));
}
predicted = y.dataSync();
});
let predictTime = (performance.now() - predictMs) / 1000;
document.getElementById("predictStatus").textContent = `Iteration ${i+1} of ${iterations}: Predict time ${predictTime.toFixed(3)}s`;
const sinValues = [[], []];
for (let i = 0; i < T; i++) {
sinValues[0].push({"x": i, "y": varray[i]});
sinValues[1].push({"x": i, "y": predicted[i]});
}
const sinContainer = document.getElementById("sinChart");
tfvis.render.linechart(
sinContainer,
{values: sinValues, series: ["varidation", "predicted"]},
{
width: 300,
height: 300,
xLabel: "time",
yLabel: "value"
});
if(model.stopTraining){
break;
}
}
})();
_debugHTML("tfjs sin E N D");
</script>
</body>
</html>
まず、 tensor_sin() ファンクションは、 tfjs-vis で sin 波を。スマホのテキストエディタで。 にて作成した sin 波のデータをテンソルで返す関数です。そして、 EarlyStopping クラスは、tfjs で Early Stopping。スマホのテキストエディタで。にて作成した Early Stopping を判断するためのクラスです。
sin 波のデータは、3つ用意しました。訓練用のデータ(ノイズあり)と、検証用のデータ(ノイズあり)、そしてノイズなしのデータとしました。
そして、訓練用と検証用のデータについて、時系列データを作成します。連続する 25 の個々のデータをひとつの時系列データとします。それをひとつずつずらしながら繰り返して作成いきます。また、ノイズなしのデータは、26 番目のデータからひとつずつ取っていきます。連続する 25 のデータからそのひとつ先の値を学習して予測することになります。
tf.slice() は、テンソルから指定された位置で切り出します。 tf.stack() は、複数の同形のテンソルをひとつのテンソルにまとめます。テンソル(オブジェクト)を JavaScript の配列の要素にしたものが引数となります。 tf.reshape() は、テンソルを変形します。 tf.concat() は、テンソルを連結します。
モデルの fit と predict の組み合わせで1イテレーションとして、これを 100 回繰り返します。エポック数は1としました。これで学習の進み具合を頻繁に確認できるようにしました。
なお、 このページが表示されると、イテレーション 10 回で学習を開始して、学習の進み具合を更新しています。10 回では不完全なのですが、良さそうに進行していることは感じられると思います。
ここまで、まだまだ不慣れで、どうなんだろう?、ということがまだまだあります。それでも、 TensorFlow.js (tfjs) で、ファーストステップ、ひとつの目標に届いたかな思います。
さて、今回使用している環境は、Android スマホのテキストエディタアプリ QuickEdit Text Editor Pro です。プレビュー機能で HTML ドキュメントの JavaScript コードを実行することで確認しています。 QuickEdit Text Editor Pro で TensorFlow.js の CODE SAMPLE FOR SCRIPT TAG SETUP をプレビュー で記事投稿した方法です。文法 Error の Debug はできませんが、console.log を出力するためのコードを追加しています。