05
コードのポイント解説
TensorFlow.js で LSTM を実装するとき、初心者がつまずきやすいポイントを中心に解説します。
スライディングウィンドウでデータを準備する
// 時系列データを [入力ウィンドウ, 正解ラベル] の組に変換する
function createWindows(data, windowSize) {
const X = [], Y = [];
for (let i = 0; i < data.length - windowSize; i++) {
X.push(data.slice(i, i + windowSize)); // 過去 N ステップ
Y.push(data[i + windowSize]); // 翌ステップの値(正解)
}
return { X, Y };
}
// LSTM に渡すには 3D テンソル [samples, timesteps, features] が必要
const xTensor = tf.tensor3d(X, [X.length, windowSize, 1]);
const yTensor = tf.tensor2d(Y, [Y.length, 1]);
Dense 層は 2D テンソル(バッチ × 特徴量)を受け取りますが、
LSTM は 3D テンソル [samples, timesteps, features] を必要とします。
tf.tensor3d で形状を明示的に指定するのが重要なポイントです。
LSTM モデルの構築(層数を動的に変える)
function buildModel(windowSize, lstmUnits, numLayers, dropoutRate) {
const model = tf.sequential();
for (let i = 0; i < numLayers; i++) {
const isFirst = i === 0;
const isLast = i === numLayers - 1;
```
model.add(tf.layers.lstm({
units: lstmUnits,
```
returnSequences: !isLast, // 最終層だけ false — 重要!
inputShape: isFirst ? [windowSize, 1] : undefined,
}));
model.add(tf.layers.dropout({ rate: dropoutRate }));
}
model.add(tf.layers.dense({ units: 16, activation: ‘relu’ }));
model.add(tf.layers.dense({ units: 1 })); // 出力は1値(次のレート)
model.compile({
optimizer: tf.train.adam(0.001),
loss: ‘meanSquaredError’
});
return model;
}
returnSequences: true は「次のLSTM層にシーケンス全体を渡す」設定です。
LSTM を積み重ねる場合、最後の層だけ false(= 最終ステップの出力だけ返す)にする必要があります。
ここを間違えると形状エラーが発生します。
バックエンドの選択(なぜ CPU がデフォルトか)
// ページ読み込み時に CPU バックエンドで初期化する
await tf.setBackend('cpu');
await tf.ready();
// WebGPU は navigator.gpu で事前チェックしてから切替
if (!navigator.gpu) {
showUnsupported(); // バッジを赤く表示してフォールバック
} else {
await tf.setBackend(‘webgpu’);
await tf.ready();
// 実際に切り替わったか確認(未対応時はフォールバックが起きる)
const actual = tf.getBackend();
if (actual !== ‘webgpu’) throw new Error(`フォールバック: ${actual}`);
}
WebGL(GPU)バックエンドは大きな行列演算では有利ですが、CPU↔GPU のデータ転送コストが発生します。
このモデルの入力は最大 40×1、LSTM Units は最大 128 と非常に小さいため、
転送オーバーヘッドが演算時間を上回り CPU の方が高速になります。
WebGPU バックエンドは Chrome 113+ / HTTPS 環境が必要で、navigator.gpu の存在で事前チェックできます。
対応環境であれば WebGL より低オーバーヘッドで動作しますが、このモデルサイズでは CPU が依然として優位です。
デモのトグルで3つを切り替えて TRAIN TIME を比較してみてください。
学習ループ(進捗表示 + UI フリーズ防止)
await model.fit(xTrain, yTrain, {
epochs,
batchSize: 32,
validationSplit: 0.1,
callbacks: {
onEpochEnd: async (epoch, logs) => {
lossHistory.push(logs.loss);
valLossHistory.push(logs.val_loss);
```
// プログレスバーとステータスを更新
const pct = ((epoch + 1) / epochs * 100).toFixed(0);
setProgress(pct);
setStatus(`学習中 ${pct}% — loss: ${logs.loss.toFixed(6)}`, 'run');
```
await tf.nextFrame(); // ← UIスレッドに制御を返す(必須)
}
}
});
await tf.nextFrame() がないと、学習中はページ全体がフリーズします。
各エポック終了後にブラウザのレンダリングサイクルに制御を返すことで、
プログレスバーやステータスのリアルタイム更新が可能になります。
多ステップ予測(再帰的な予測)
function multiStepPredict(model, seedWindow, steps, minVal, maxVal) {
let window = [...seedWindow]; // 直近 N ステップのコピー
const predictions = [];
for (let s = 0; s < steps; s++) {
// [1, windowSize, 1] の形状で入力テンソルを作成
const input = tf.tensor3d([window.map(v => [v])], [1, window.length, 1]);
const normPred = model.predict(input).dataSync()[0];
input.dispose();
```
// 逆正規化して実際のレートに変換
const realPred = normPred * (maxVal - minVal) + minVal;
predictions.push(realPred);
```
// 予測値をウィンドウに追加して次のステップへ(自己回帰)
window = […window.slice(1), normPred];
}
return predictions;
}
ハイライト行が「自己回帰(autoregressive)」の核心です。
予測した値を次の入力ウィンドウの末尾に追加することで、連続的に先の未来を予測できます。
ただしステップが増えるほど誤差が積み重なるため、遠い未来の予測精度は下がります。
評価指標(RMSE・MAPE)の計算
// RMSE: 予測誤差の大きさを実際のレートの単位(円)で表す
function calcRMSE(actual, predicted) {
const mse = actual.reduce((s, v, i) =>
s + (v - predicted[i]) ** 2, 0
) / actual.length;
return Math.sqrt(mse);
}
// MAPE: 誤差をパーセントで表す(スケールに依存しない比較ができる)
function calcMAPE(actual, predicted) {
const sum = actual.reduce((s, v, i) =>
s + Math.abs((v - predicted[i]) / v), 0
);
return (sum / actual.length) * 100;
}
RMSE が「1.5」なら、平均的に±1.5円の誤差があるということです。
MAPE は「1%」なら実際の値の1%程度の誤差、という意味で、
為替レートのスケールが変わっても比較しやすい指標です。
⚠️ 免責事項
本デモはTensorFlow.jsの学習・実験を目的としたものです。
デモの予測結果は実際の為替市場の動向を予測するものではなく、
投資判断や取引の根拠として使用しないでください。
為替取引はリスクを伴い、元本割れの可能性があります。
投資判断はご自身の責任で行い、必要に応じて専門家にご相談ください。