結論

LSTMとは、3つのゲートで「何を覚え、何を忘れるか」を学ぶ、長い時系列に強いRNNです。長い間あいた情報も、セル状態に載せて運べます。

この記事でわかること

  • 3つのゲートを7ステップ計算した値の表
  • 長さ10〜100の記憶課題で、RNN・GRU・LSTMの誤差
  • 入力の形(3次元)とエラー原文9種の直し方

2026年10月23日更新|NumPy 2.4.4・Keras 3.15.1で実行

LSTM は、ふつうの RNN(前の時刻の結果を次の時刻に渡すニューラルネットワーク)に、記憶を運ぶ「セル状態」と、その出し入れを決める3つのゲートを足したものです。

flowchart TD
    X[入力 x_t<br>前の出力 h_t−1] --> F[忘却ゲート f<br>前の記憶を残す量]
    X --> I[入力ゲート i<br>新しく足す量]
    F --> C[セル状態 c_t<br>長く運ぶ記憶]
    I --> C
    C --> O[出力ゲート o<br>外に出す量]
    O --> H[出力 h_t<br>次の時刻へ]

急いでいる人向けに、よく調べられることの答えを先にまとめます。

知りたいこと 答え 詳しい節
LSTMとは ゲートで記憶を出し入れする RNN LSTMとは
読み方・正式名 エルエスティーエム。Long Short-Term Memory LSTMとは
RNN との違い 長い間あいた依存も学べる RNNとの違い
3つのゲート 忘却・入力・出力。0〜1の値で量を調節 仕組み
なぜ長く覚えられる セル状態は f を掛けて足すだけで次へ渡る 勾配が消えにくい理由
GRU との違い GRU はゲート2つで軽い。精度は課題しだい GRUとの違い
Transformer との違い 1つずつ順に読むか、全部を同時に見比べるか Transformerとの違い
入力の形 (バッチ, 時系列長, 特徴量) の3次元 入力の形
よくあるエラー 2次元のまま渡す・特徴量の数が違う エラーと直し方

確認環境:Ubuntu 24.04.4 LTS(Linux カーネル 6.18)・Python 3.11.15・NumPy 2.4.4・Keras 3.15.1(バックエンドは JAX 0.4.30 の CPU 版)。LSTM と RNN の順伝播・誤差逆伝播・Adam を NumPy だけで書き、数値微分との照合、ゲートの1ステップずつの計算、勾配の大きさの測定、足し算の記憶課題とサイン波の学習(乱数の種 390〜392)を実行しました。Keras では、NumPy の LSTM と同じ重みを入れて出力が一致することを確かめ、SimpleRNN・GRU・LSTM の学習、出力の形、形が合わないときのエラー文を実行して載せています。PyTorch のコードは PyTorch 2.14 の公式ドキュメント(torch.nn.LSTM・torch.nn.GRU)の引数と形に合わせて書いたもので、エラー文は PyTorch の GitHub のソース(torch/nn/modules/rnn.py)から引用しました。Keras の引数は keras.io の公式ドキュメント、歴史と使われた例は英語版 Wikipedia と各論文(Hochreiter・Schmidhuber 1997、Chung ら 2014、Vaswani ら 2017、Beck ら 2024)で確かめました(確認日 2026年10月23日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「LSTMとは|仕組み・3つのゲート・RNNとの違いをPythonで実演」

LSTMとは(意味・読み方・何に使うか)

この節では、LSTM が何の略で、どんなデータに使うものかが分かります。

LSTM(Long Short-Term Memory、エルエスティーエム)は、時系列や文章のように「順番のあるデータ」を扱うニューラルネットワークの一種です。 日本語では「長・短期記憶」と訳されます。名前のとおり、RNN が持つ短い記憶(short-term memory)を、長い間(long)保てるようにしたものです。

1ノートを持つ読み手

文章を1語ずつ読みながら、大事なことだけノート(セル状態)に書き、要らなくなったら消す。読み終えたとき、最初のほうの大事な語も覚えている。

23つの蛇口

ゲートは 0(閉じる)〜1(開ける)の蛇口。忘却ゲートは「残す量」、入力ゲートは「足す量」、出力ゲートは「外に見せる量」を決める。

3学習で決まる

どの蛇口をいつ開けるかは、人が決めるのではなく、データから学習した重みで決まる。

順番のあるデータとは、前後の並びに意味があるデータのことです。

データ 1ステップの中身 LSTM にさせること
時系列(気温・売上・センサー) その時刻の値(1個〜複数) 次の値の予測、異常の検知
文章 1語(1トークン)を表すベクトル 分類、翻訳、次の語の予測
音声 短い区間の音の特徴 文字起こし、話者の判定
操作の記録 1回の操作 次の行動の予測

ニューラルネットワーク自体の仕組み(重み・活性化関数・誤差逆伝播)は、関連記事「ニューラルネットワークとは|仕組み・学習・AttentionをNumPyで実演」で扱っています。この記事はその続きとして、順番のあるデータを扱う RNN と LSTM に絞ります。

RNNとは?LSTMが必要になった理由(長期の依存と勾配消失)

この節では、LSTM の元になった RNN の仕組みと、RNN が長い時系列を苦手にする理由を、測った数字で確かめます。

RNN の仕組み:前の時刻の結果を次に渡す

RNN(Recurrent Neural Network、再帰型ニューラルネットワーク)は、同じ計算を1ステップずつくり返し、前のステップの出力(隠れ状態 h)を次のステップの入力に足すネットワークです。 式は文字で書くと次の1行だけです。

h_t = tanh( W・x_t + U・h_{t−1} + b )
  x_t   : 時刻 t の入力
  h_{t−1}: 1つ前の時刻の隠れ状態(RNN の「記憶」)
  W, U, b: 学習で決まる重みとバイアス(すべての時刻で同じものを使う)

時刻ごとに横に並べて描くと、同じ箱(同じ重み)に h が順に受け渡されていく形になります。

flowchart TD
    X1[入力 x_1] --> A1[RNN 時刻1]
    A1 -->|h_1| A2[RNN 時刻2]
    X2[入力 x_2] --> A2
    A2 -->|h_2| A3[RNN 時刻3]
    X3[入力 x_3] --> A3
    A3 --> Y[出力]

長期の依存とは

「長期の依存」は、答えを出すのに、ずっと前の入力が必要になることです。たとえば「私はフランスで生まれ育ち、(中略)……なので、__語を話します。」の空欄には、何十語も前の「フランス」が効きます。RNN の h はステップごとに新しい入力と混ぜられて上書きされるので、ずっと前の情報ほど薄れます。

勾配消失:遠い過去ほど学習の信号が届かない

学習では、出力の誤差を時刻を逆向きにたどって各時刻へ配ります(BPTT。時間方向の誤差逆伝播)。RNN では、1ステップ戻るたびに「tanh の傾き(1 以下)× 重み U」を掛けるので、遠い時刻に届く信号は掛け算の回数だけ小さくなります。 これが勾配消失です(逆に大きくなり続けるのが勾配爆発)。

NumPy で書いた RNN と LSTM(隠れ状態32個、初期値は同じ範囲の一様乱数)で、出力を「最初の時刻の入力 x_1」と「最後の時刻の入力 x_T」で微分した大きさを測りました。時系列の長さ T を変えています(測ったコードは後の節にあります)。

長さ T RNN:x_1 への勾配 LSTM:x_1 への勾配 参考:x_T への勾配
10 9.77e-04 1.36e-03 約 3e-02(どちらも)
30 8.06e-08 6.43e-05 約 3e-02
50 1.51e-11 2.51e-06 約 3e-02
100 6.51e-22 9.39e-10 約 3e-02

学習を始める前の RNN では、100 ステップ前の入力に届く勾配が 6.51e-22(10 のマイナス22乗)まで小さくなりました。 最後の時刻(約 0.03)と比べると、ほぼ0です。これでは、100 ステップ前の入力をどう使えばよいかを学べません。LSTM も学習前は小さくなりますが、減り方がずっと緩やかです。さらに学習が進むと差が広がることを、後の節で確かめます。

RNN と LSTM の記憶の持ち方

RNN記憶は h の1本だけ。毎ステップ tanh(W・x + U・h) で作り直すので、前の情報は混ぜられて薄れる。勾配も毎回 U と tanh の傾きを掛けて小さくなる
→
LSTM出力用の h とは別に、記憶専用のセル状態 c を持つ。c は「f を掛けて、必要な分を足す」だけで次へ渡るので、f が1に近ければ情報も勾配もそのまま届く

1997年に Hochreiter と Schmidhuber が、この「誤差が減らずに流れる道」として LSTM を提案した

LSTMの仕組み:セル状態と3つのゲート(忘却・入力・出力)

この節では、LSTM の1ステップの中で何が計算されているかを、式と図で順に見ます。

LSTM の1ステップは、「3つのゲートの開き具合を決める → セル状態を更新する → 出力を出す」の3段です。 ゲートは、シグモイド関数(どんな数も 0〜1 に押し込む関数)で作る 0〜1 の値で、掛け算で「通す割合」を決めます。

(σ はシグモイド関数、tanh は −1〜1 に押し込む関数、⊙ は要素ごとの掛け算)
i_t = σ( W_i・x_t + U_i・h_{t−1} + b_i )      入力ゲート:新しい情報をどれだけ書き込むか
f_t = σ( W_f・x_t + U_f・h_{t−1} + b_f )      忘却ゲート:前のセル状態をどれだけ残すか
g_t = tanh( W_g・x_t + U_g・h_{t−1} + b_g )   候補:書き込む中身(PyTorch では cell gate と呼ぶ)
o_t = σ( W_o・x_t + U_o・h_{t−1} + b_o )      出力ゲート:セル状態をどれだけ外に出すか
c_t = f_t ⊙ c_{t−1} + i_t ⊙ g_t               セル状態の更新(残す分+足す分)
h_t = o_t ⊙ tanh( c_t )                        出力(隠れ状態)

この6つの式は、PyTorch 2.14 の torch.nn.LSTM の公式ドキュメントに載っている式と同じ形です(PyTorch はバイアスを入力側と隠れ状態側の2つに分けて書いています)。

flowchart TD
    C0[前のセル状態 c_t−1] --> M[× f_t<br>残す分だけ残す]
    M --> A[+ i_t × g_t<br>新しい分を足す]
    A --> C1[セル状態 c_t<br>次の時刻へ]
    C1 --> T[tanh して × o_t<br>出す分だけ出す]
    T --> H[出力 h_t]
部品 値の範囲 役割 0 に近いとき 1 に近いとき
忘却ゲート f 0〜1 前の記憶を残す割合 前の記憶を消す そのまま残す
入力ゲート i 0〜1 新しい情報を足す割合 足さない 候補をそのまま足す
候補 g −1〜1 書き込む中身 — —
出力ゲート o 0〜1 記憶を外に見せる割合 h が0(見せない) tanh(c) をそのまま出す
セル状態 c 制限なし 長く運ぶ記憶 — —
隠れ状態 h −1〜1 その時刻の出力 — —

大事なのは c_t の式です。 c は行列を掛けたり tanh に通したりせず、「f を掛けて、i × g を足す」だけで次の時刻へ渡ります。f が1に近く i が0に近ければ、c は何ステップたってもほぼ同じ値のままです。

重みはいくつあるか(パラメーター数)

4つの部品(i・f・g・o)がそれぞれ W・U・b を持つので、LSTM の重みは同じ大きさの RNN の4倍です。入力の特徴量を D 個、隠れ状態を H 個とすると、次の式になります。

RNN  : H×(D+H) + H
GRU  : 3 × { H×(D+H) + H }   (バイアスを1組で数えた場合)
LSTM : 4 × { H×(D+H) + H }   (PyTorch はバイアスが2組なので、さらに 4H 多い)
D=1   H=16  RNN     288  GRU     864  LSTM   1,152  (PyTorch の LSTM   1,216)
D=2   H=32  RNN   1,120  GRU   3,360  LSTM   4,480  (PyTorch の LSTM   4,608)
D=8   H=64  RNN   4,672  GRU  14,016  LSTM  18,688  (PyTorch の LSTM  18,944)
D=300 H=256 RNN 142,592  GRU 427,776  LSTM 570,368  (PyTorch の LSTM 571,392)

Keras の model.summary() でも、D=2・H=32 の LSTM 層は 4,480、SimpleRNN 層は 1,120 で、式と一致しました。Keras の GRU 層は既定でバイアスを2組持つので 3,456 でした(実行結果は後の節)。

LSTMのゲートの計算を1ステップずつ追う(NumPy)

この節では、ゲートが実際にどんな値をとり、セル状態がどう変わるかを、手で決めた重みで1ステップずつ計算して確かめます。

隠れ状態を1個だけにした LSTM に、「値」「書き込み合図」「読み出し合図」の3つを入力します。1ステップ目に書き込んだ値(0.8)を、途中の値に惑わされずに覚えておき、7ステップ目の読み出し合図で出せるかを見ます。重みは、ゲートの役割がはっきり出るように手で決めました(学習で決めた値ではありません)。

"""LSTM のゲートを1ステップずつ計算する(隠れ状態1個、重みは手で決めた値)"""
import numpy as np
sig = lambda z: 1 / (1 + np.exp(-z))

# 入力 x = [値, 書き込み合図, 読み出し合図]
W = {  #        値   書込  読出   バイアス
    "i": (np.array([0.0, 10.0, 0.0]), -5.0),   # 書き込み合図のときだけ開く
    "f": (np.array([0.0, -10.0, 0.0]), 5.0),   # ふだんは開きっぱなし(=覚え続ける)。書き込むときだけ閉じる
    "g": (np.array([1.0, 0.0, 0.0]), 0.0),     # 書き込む候補 = tanh(値)
    "o": (np.array([0.0, 0.0, 10.0]), -5.0),   # 読み出し合図のときだけ開く
}
xs = [[0.8, 1, 0], [0.3, 0, 0], [-0.5, 0, 0], [0.9, 0, 0], [-0.7, 0, 0], [0.2, 0, 0], [0.1, 0, 1]]
c = 0.0; h = 0.0
print(" t   値   書 読 |   i      f      g      o   |   c       h")
for t, x in enumerate(xs, 1):
    x = np.array(x, dtype=float)
    a = {k: w @ x + b for k, (w, b) in W.items()}       # 今回は h_{t-1} の重みを0にしている
    i, f, o = sig(a["i"]), sig(a["f"]), sig(a["o"]); g = np.tanh(a["g"])
    c = f * c + i * g
    h = o * np.tanh(c)
    print(f"{t:>2} {x[0]:>5.1f}  {int(x[1])}  {int(x[2])} | {i:.4f} {f:.4f} {g:+.4f} {o:.4f} | {c:+.4f} {h:+.4f}")

# 同じ入力を、ゲートの無い RNN(h = tanh(1.0×値 + 0.9×h))に通す
h = 0.0
print("\nRNN: t と h")
for t, x in enumerate(xs, 1):
    h = np.tanh(1.0 * x[0] + 0.9 * h)
    print(f"{t:>2} {h:+.4f}")
 t   値   書 読 |   i      f      g      o   |   c       h
 1   0.8  1  0 | 0.9933 0.0067 +0.6640 0.0067 | +0.6596 +0.0039
 2   0.3  0  0 | 0.0067 0.9933 +0.2913 0.0067 | +0.6571 +0.0039
 3  -0.5  0  0 | 0.0067 0.9933 -0.4621 0.0067 | +0.6496 +0.0038
 4   0.9  0  0 | 0.0067 0.9933 +0.7163 0.0067 | +0.6501 +0.0038
 5  -0.7  0  0 | 0.0067 0.9933 -0.6044 0.0067 | +0.6417 +0.0038
 6   0.2  0  0 | 0.0067 0.9933 +0.1974 0.0067 | +0.6387 +0.0038
 7   0.1  0  1 | 0.0067 0.9933 +0.0997 0.9933 | +0.6351 +0.5578

RNN: t と h
 1 +0.6640
 2 +0.7151
 3 +0.1426
 4 +0.7733
 5 -0.0041
 6 +0.1939
 7 +0.2678

1ステップ目と7ステップ目を、式に数字を入れて追います。

1

t=1 書き込む:ゲートを決める

書き込み合図が1なので、i = σ(10×1 − 5) = σ(5) = 0.9933(開く)、f = σ(−10×1 + 5) = 0.0067(閉じる)。候補は g = tanh(0.8) = 0.6640。

i・f・g

2

t=1 セル状態を更新する

c = f × c前 + i × g = 0.0067 × 0 + 0.9933 × 0.6640 = 0.6596。値 0.8 がセルに書き込まれた。

c

3

t=2〜6 覚え続ける

合図がないので i = 0.0067(ほぼ足さない)、f = 0.9933(ほぼ残す)。値が 0.9 や −0.7 に揺れても、c は 0.66 → 0.64 とほとんど変わらない。

f≈1

4

t=7 読み出す

読み出し合図で o = 0.9933。h = o × tanh(c) = 0.9933 × tanh(0.6351) = 0.5578。それまで h は 0.004 前後で、記憶は外に出ていなかった。

o

LSTM は、7ステップ前に書いた値をセル状態に保ち、合図のときだけ出せました。 同じ入力を通したゲートの無い RNN の h は、0.66 → 0.72 → 0.14 → 0.77 → −0.004 と、毎回の値に引きずられて変わり、7ステップ目の 0.2678 には1ステップ目の 0.8 の影響がほとんど残っていません。実際の LSTM では、この重みを人が決めるのではなく、データから学習します。学習した LSTM の中で同じことが起きているかは、次の節で確かめます。

LSTMはなぜ勾配が消えにくいのか(実測)

この節では、LSTM が長い依存を学べる理由を、誤差逆伝播のコードと、学習の前後で測った勾配・ゲートの値で確かめます。

セル状態の道は「f を掛けるだけ」で戻る

誤差逆伝播で、時刻 t のセル状態の勾配を1つ前の時刻へ戻す計算は、NumPy で書くと次の1行です(全体のコードは次の節)。

dc = dc * f   # セル状態の道:忘却ゲートを掛けるだけで戻る

RNN では1ステップ戻るたびに「重み U と tanh の傾き」を掛けますが、LSTM のセル状態の道では f を掛けるだけです(ゲートを通る別の道もあり、そちらは RNN と同じように小さくなります)。f が1に近いあいだは、勾配が何ステップ戻ってもほとんど減りません。 この「誤差が一定のまま流れる道」を、提案した論文は constant error carousel(誤差の回転木馬)と呼んでいます。

学習の前と後で、遠い入力への勾配を測る

足し算の記憶課題(次の節で説明。長さ50と100)で学習させる前と後に、出力を最初の時刻の入力 x_1 で微分した大きさを測りました。

課題の長さ モデル 学習前 学習後(4,000回)
50 RNN 1.51e-11 2.77e-04
50 LSTM 2.51e-06 1.98e-01
100 RNN 6.51e-22 4.30e-41
100 LSTM 9.39e-10 3.14e-01

長さ100では、学習後の LSTM は x_1 への勾配が 0.314 と大きくなり、RNN は 4.30e-41 とさらに小さくなりました。 LSTM は学習の中で「遠い入力を使う道」を作り、RNN は作れなかったということです。

学習した LSTM のゲートを見る

長さ50の課題で学習した LSTM(隠れ状態32個)について、合図のない時刻と合図のある時刻で、ゲートの平均を比べました。

学習前: f の平均 0.733  0.95以上のユニット 0/32  最大 0.7503
学習後: f の平均 0.752  0.95以上のユニット 1/32  最大 0.9760
テスト MSE 0.00012823223232061691
ユニット22: 合図なしの f 0.976 / 合図ありの f 0.907  合図なしの i 0.038 / 合図ありの i 0.826
ユニット11: 合図なしの f 0.950 / 合図ありの f 0.866  合図なしの i 0.096 / 合図ありの i 0.458
ユニット31: 合図なしの f 0.949 / 合図ありの f 0.931  合図なしの i 0.151 / 合図ありの i 0.457
例: 合図の位置 [24, 37] 値 [0.985, 0.108] 正解 1.093
  t= 1 値 0.005 合図 0  c[22] = -0.0028
  t=24 値 0.686 合図 0  c[22] = +0.1936
  t=25 値 0.985 合図 1  c[22] = +0.4637
  t=26 値 0.872 合図 0  c[22] = +0.4640
  t=31 値 0.357 合図 0  c[22] = +0.4395
  t=37 値 0.437 合図 0  c[22] = +0.4199
  t=38 値 0.108 合図 1  c[22] = +0.1823
  t=39 値 0.519 合図 0  c[22] = +0.1805
  t=50 値 0.836 合図 0  c[22] = +0.1699

学習前の f は、初期値(忘却ゲートのバイアスを1にした。Keras の既定 unit_forget_bias=True と同じ考え方)の σ(1) = 0.73 前後でした。学習後、ユニット22は、合図のない時刻の f が 0.976、i が 0.038 になり、合図の時刻だけ i が 0.826 に開くようになりました。前の節で手で決めた「書き込み合図で開き、ふだんは覚え続ける」形を、データから学んだことになります。c[22] は合図の時刻(t=25)で 0.19 → 0.46 に上がり、その後12ステップ、値が変わっても 0.46 → 0.42 とほぼ保たれました。2つ目の合図(t=38)では別の値に書きかわっていて、最終的な答えは複数のユニットを組み合わせて出しています。

RNNとLSTMを学習させて比べる(足し算の記憶課題・サイン波)

この節では、同じ条件で RNN・LSTM(と GRU)を学習させ、長い依存が要る課題と要らない課題で、誤差がどう違うかを実測します。

2つの課題

足し算の記憶課題(長い依存が要る)

各時刻に0〜1の乱数の「値」と、0か1の「合図」を入れる

合図は前半に1つ、後半に1つ

最後に「合図が付いた2つの値の和」を答える

長さ T を 10・30・50・100 と変える

サイン波の続き(短い依存で解ける)

周期15〜25のサイン波に、標準偏差0.05の雑音を足す

直前の20個の値を入れる

次の1個の値を答える

直近の数個を見れば答えの見当がつく

足し算の記憶課題は、LSTM を提案した1997年の論文でも使われた課題を、合図の位置が前半・後半に分かれるようにして作りました。何も覚えずに「いつも1と答える」と、誤差(平均二乗誤差 MSE)は約0.167 になります(2つの乱数の和の分散 1/6。テスト用の2,000件では 0.1653〜0.1715)。これより小さくならなければ、合図の値を覚えられていないことになります。

NumPy で書いた RNN と LSTM(全コード)

"""NumPy だけで書いた RNN と LSTM(順伝播・誤差逆伝播・Adam)"""
import numpy as np

def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-z))

class RNN:
    """h_t = tanh(x_t Wx + h_{t-1} Wh + b)。最後の h_T から y = h_T Wy + by を出す"""
    def __init__(self, D, H, O=1, seed=0):
        r = np.random.default_rng(seed); k = 1 / np.sqrt(H)
        self.p = {"Wx": r.uniform(-k, k, (D, H)), "Wh": r.uniform(-k, k, (H, H)),
                  "b": np.zeros(H), "Wy": r.uniform(-k, k, (H, O)), "by": np.zeros(O)}
        self.H = H

    def forward(self, x):                      # x: (バッチ N, 時系列長 T, 特徴量 D)
        N, T, D = x.shape; p = self.p
        h = np.zeros((N, self.H)); self.hs = [h]; self.x = x
        for t in range(T):
            h = np.tanh(x[:, t] @ p["Wx"] + h @ p["Wh"] + p["b"])
            self.hs.append(h)
        return h @ p["Wy"] + p["by"]           # (N, O)

    def backward(self, dy):
        p = self.p; g = {k: np.zeros_like(v) for k, v in p.items()}
        hT = self.hs[-1]; g["Wy"] = hT.T @ dy; g["by"] = dy.sum(0)
        dh = dy @ p["Wy"].T; N, T, D = self.x.shape; dx = np.zeros_like(self.x)
        for t in reversed(range(T)):
            h, hp = self.hs[t + 1], self.hs[t]
            da = dh * (1 - h * h)
            g["Wx"] += self.x[:, t].T @ da; g["Wh"] += hp.T @ da; g["b"] += da.sum(0)
            dx[:, t] = da @ p["Wx"].T
            dh = da @ p["Wh"].T
        self.dx = dx
        return g

class LSTM:
    """ゲートの並びは PyTorch と同じ i(入力)・f(忘却)・g(候補)・o(出力)"""
    def __init__(self, D, H, O=1, seed=0, forget_bias=1.0):
        r = np.random.default_rng(seed); k = 1 / np.sqrt(H)
        b = np.zeros(4 * H); b[H:2 * H] = forget_bias   # Keras の unit_forget_bias=True と同じ考え方
        self.p = {"Wx": r.uniform(-k, k, (D, 4 * H)), "Wh": r.uniform(-k, k, (H, 4 * H)),
                  "b": b, "Wy": r.uniform(-k, k, (H, O)), "by": np.zeros(O)}
        self.H = H

    def forward(self, x):
        N, T, D = x.shape; p = self.p; H = self.H
        h = np.zeros((N, H)); c = np.zeros((N, H)); self.x = x; self.cache = []
        self.hs = [h]; self.cs = [c]
        for t in range(T):
            a = x[:, t] @ p["Wx"] + h @ p["Wh"] + p["b"]
            i = sigmoid(a[:, :H]); f = sigmoid(a[:, H:2*H])
            g = np.tanh(a[:, 2*H:3*H]); o = sigmoid(a[:, 3*H:])
            c_new = f * c + i * g
            tc = np.tanh(c_new); h_new = o * tc
            self.cache.append((i, f, g, o, c, tc, h))
            h, c = h_new, c_new; self.hs.append(h); self.cs.append(c)
        return h @ p["Wy"] + p["by"]

    def backward(self, dy):
        p = self.p; H = self.H; g_ = {k: np.zeros_like(v) for k, v in p.items()}
        hT = self.hs[-1]; g_["Wy"] = hT.T @ dy; g_["by"] = dy.sum(0)
        dh = dy @ p["Wy"].T; dc = np.zeros_like(dh); N, T, D = self.x.shape
        dx = np.zeros_like(self.x)
        for t in reversed(range(T)):
            i, f, g, o, c_prev, tc, h_prev = self.cache[t]
            do = dh * tc
            dc = dc + dh * o * (1 - tc * tc)
            di = dc * g; dg = dc * i; df = dc * c_prev
            da = np.concatenate([di * i * (1 - i), df * f * (1 - f),
                                 dg * (1 - g * g), do * o * (1 - o)], axis=1)
            g_["Wx"] += self.x[:, t].T @ da; g_["Wh"] += h_prev.T @ da; g_["b"] += da.sum(0)
            dx[:, t] = da @ p["Wx"].T
            dh = da @ p["Wh"].T
            dc = dc * f                       # セル状態の道:忘却ゲートを掛けるだけで戻る
        self.dx = dx
        return g_

class Adam:
    def __init__(self, params, lr=0.005, clip=1.0):
        self.p = params; self.lr = lr; self.clip = clip; self.t = 0
        self.m = {k: np.zeros_like(v) for k, v in params.items()}
        self.v = {k: np.zeros_like(v) for k, v in params.items()}

    def step(self, grads):
        norm = np.sqrt(sum((g * g).sum() for g in grads.values()))
        s = min(1.0, self.clip / (norm + 1e-12)); self.t += 1      # 勾配の大きさを1までに切る
        for k in self.p:
            gk = grads[k] * s
            self.m[k] = 0.9 * self.m[k] + 0.1 * gk
            self.v[k] = 0.999 * self.v[k] + 0.001 * gk * gk
            mh = self.m[k] / (1 - 0.9 ** self.t); vh = self.v[k] / (1 - 0.999 ** self.t)
            self.p[k] -= self.lr * mh / (np.sqrt(vh) + 1e-8)
        return norm

自分で書いた誤差逆伝播は、まず数値微分(重みを ±0.000001 ずらして損失の差をとる方法)と比べて、計算が正しいことを確かめます。

RNN: パラメーター 33 個  相対誤差の最大 1.22e-08
LSTM: パラメーター 117 個  相対誤差の最大 7.79e-07

すべての重みで、相対誤差が 1e-06 より小さくなりました。 さらに、この NumPy の LSTM と Keras の LSTM 層に同じ重みを入れて、同じ入力を通しました。

import numpy as np, keras
from nets import LSTM              # 上のコードを nets.py として保存したもの

D, H, N, T = 3, 5, 4, 7
x = np.random.default_rng(390).normal(size=(N, T, D)).astype("float32")
mine = LSTM(D=D, H=H, seed=390)
layer = keras.layers.LSTM(H, return_sequences=True, return_state=True)
layer.build((None, T, D))
print("Keras の重みの形:", [tuple(w.shape) for w in layer.get_weights()])
layer.set_weights([mine.p["Wx"], mine.p["Wh"], mine.p["b"]])   # 並びは i・f・c(g)・o で同じ
seq, h_last, c_last = layer(x)
mine.forward(x.astype("float64"))
print("全時刻の h の差の最大:", float(np.abs(np.asarray(seq) - np.stack(mine.hs[1:], axis=1)).max()))
print("最後の c の差の最大:  ", float(np.abs(np.asarray(c_last) - mine.cs[-1]).max()))
Keras の重みの形: [(3, 20), (5, 20), (20,)]
全時刻の h の差の最大: 6.923011613979568e-08
最後の c の差の最大:   6.166450394751166e-08

Keras の LSTM と、7時刻すべての h が 6.9e-08 以内で一致しました(Keras は 32 ビットの小数で計算するので、この程度の差が出ます)。Keras の重みは、入力側が (特徴量, 4×H)、隠れ状態側が (H, 4×H)、バイアスが (4×H) の3つで、ゲートの並びも NumPy の実装と同じでした。

学習の条件と、課題を作るコード

def adding(n, T, rng):
    """足し算の記憶課題:値の列のうち、合図が付いた2か所の値の和を最後に答える"""
    v = rng.random((n, T)); m = np.zeros((n, T))
    a = rng.integers(0, T // 2, n); b = rng.integers(T // 2, T, n)   # 前半に1つ、後半に1つ
    m[np.arange(n), a] = 1; m[np.arange(n), b] = 1
    x = np.stack([v, m], axis=2)                       # (N, T, 2)
    y = (v * m).sum(1, keepdims=True)                  # (N, 1)
    return x, y

rng = np.random.default_rng(390)
xt, yt = adding(2000, T, np.random.default_rng(391))   # テスト用(学習に使わない)
m = LSTM(D=2, H=32, seed=390); opt = Adam(m.p, lr=0.005)  # RNN も同じ条件
for s in range(1, 4001):
    x, y = adding(64, T, rng)
    d = m.forward(x) - y
    opt.step(m.backward(2 * d / len(x)))               # MSE の勾配
print("テスト MSE", ((m.forward(xt) - yt) ** 2).mean())
条件 値
隠れ状態の数 32(RNN 1,153 個・LSTM 4,513 個の重み)
学習 Adam(学習率 0.005)、勾配の大きさを1までに切る、64件ずつ 4,000 回
評価 学習に使わない 2,000 件のテスト MSE
乱数の種 390(長さ50と100は 391・392 でもくり返した)

結果:長さ100で RNN は学べず、LSTM は学べた

(長さ T、モデル、重みの数、テスト MSE、学習時間(秒)、500回ごとのテスト MSE の一部)
RNN  10 1153 0.00067  3.1 (1, 1.0548) (500, 0.0269) (1000, 0.021) (2000, 0.0016) (4000, 0.0007)
LSTM 10 4513 0.00012  8.6 (1, 1.001) (500, 0.0042) (1000, 0.0011) (2000, 0.0001) (4000, 0.0001)
RNN  30 1153 0.00529  5.7 (1, 1.1128) (500, 0.1672) (1000, 0.171) (1500, 0.025) (4000, 0.0053)
LSTM 30 4513 5e-05   25.6 (1, 1.0386) (500, 0.0073) (1000, 0.0006) (2000, 0.0001) (4000, 0.0001)
RNN  50 1153 0.05283 11.1 (1, 1.0982) (500, 0.1658) (3000, 0.1576) (3500, 0.0877) (4000, 0.0528)
LSTM 50 4513 0.00013 40.4 (1, 1.0251) (500, 0.2156) (1000, 0.0006) (2000, 0.0001) (4000, 0.0001)
RNN  100 1153 0.17315 18.9 (1, 1.1113) (500, 0.1726) (2000, 0.1728) (3500, 0.1842) (4000, 0.1732)
LSTM 100 4513 7e-05  86.9 (1, 1.0377) (500, 0.1728) (1000, 0.0073) (1500, 0.0004) (4000, 0.0001)
長さ T RNN のテスト MSE LSTM のテスト MSE 読み方
10 0.00067 0.00012 どちらも解けた
30 0.00529 0.00005 RNN は 1,000 回まで足踏み
50 0.05283(種を変えて 0.0949・0.0234) 0.00013(0.00015・0.00029) RNN は 3,000 回すぎて少し下がり始めた
100 0.17315(0.1653・0.1697) 0.00007(0.00019・0.00026) RNN は「いつも1」と同じ誤差のまま

長さ100の記憶課題のテスト MSE(NumPy・3回の平均)いつも1と答えたときの 0.1715 を100%とした長さ

いつも10.1715
RNN0.1694
LSTM0.00017

長さ100では、RNN の誤差は3回とも「いつも1と答える」のと同じ 0.17 前後で止まり、LSTM は 0.0002 前後まで下がりました。 長さ10なら RNN でも解けて、差は長さが伸びるほど開きました。LSTM の誤差も、長さ100では最初の 500 回は 0.17 で足踏みしていて、1,000 回で 0.0073 まで下がっています。学習時間は LSTM のほうが長く、長さ100で RNN の約4.6倍(18.9 秒と 86.9 秒)でした。

Keras の SimpleRNN・GRU・LSTM で同じ課題を解く

同じ課題を、Keras の層で学習させました。条件は NumPy と同じにそろえています(隠れ状態32、Adam 0.005、勾配の大きさを1までに切る、32,000 件を 64 件ずつ 8 周 = 4,000 回)。

import numpy as np, keras
from train import adding          # 上の adding 関数

T = 100
xtr, ytr = adding(32000, T, np.random.default_rng(390))
xte, yte = adding(2000, T, np.random.default_rng(391))
for name, Layer in [("SimpleRNN", keras.layers.SimpleRNN), ("GRU", keras.layers.GRU), ("LSTM", keras.layers.LSTM)]:
    keras.utils.set_random_seed(390)
    model = keras.Sequential([keras.Input((T, 2)), Layer(32), keras.layers.Dense(1)])
    model.compile(optimizer=keras.optimizers.Adam(0.005, clipnorm=1.0), loss="mse")
    h = model.fit(xtr, ytr, batch_size=64, epochs=8, verbose=0, validation_data=(xte, yte))
    print(name, model.count_params(), [round(v, 5) for v in h.history["val_loss"]])
(長さ T=100・種 390。8周それぞれのテスト MSE)
SimpleRNN 1153 [0.17247, 0.17208, 0.19106, 0.17044, 0.18721, 0.1715, 0.17147, 0.17052]
GRU 3489 [0.00985, 0.00198, 0.00037, 0.00052, 0.00018, 9e-05, 0.0001, 0.00014]
LSTM 4513 [0.17356, 0.00842, 0.00197, 0.00037, 0.00018, 0.00016, 0.0001, 7e-05]
長さ T SimpleRNN GRU LSTM 学習時間(秒)
10 0.00072 0.00004 0.00006 2.6/5.8/8.0
50 0.15763 0.00008 0.00008 8.2/23.7/34.0
100(種 390) 0.17052 0.00014 0.00007 10.7/40.2/39.9
100(種 391) 0.16400 0.00006 0.00039 12.3/38.8/41.8
100(種 392) 0.16083 0.00005 0.00012 11.5/40.4/40.9
0.165SimpleRNN(長さ100)3回の平均。学べていない
0.00008GRU(長さ100)3回の平均
0.00019LSTM(長さ100)3回の平均

Keras でも、SimpleRNN は長さ50・100で学べず、GRU と LSTM はどちらも 0.0004 以下まで下がりました。 この課題では GRU と LSTM の差は回によって入れかわる程度で、長さ50・100では GRU のほうが1周目から誤差が小さい(LSTM は1周目が 0.17 前後)という違いがありました。学習した LSTM に、新しい5件を答えさせた結果です(長さ50・4周学習)。

合図の位置 16,35  値 0.041+0.627 = 正解 0.669  予測 0.657
合図の位置 11,38  値 0.250+0.197 = 正解 0.448  予測 0.424
合図の位置 20,31  値 0.635+0.913 = 正解 1.548  予測 1.564
合図の位置 24,28  値 0.451+0.827 = 正解 1.278  予測 1.253
合図の位置  0,48  値 0.709+0.692 = 正解 1.401  予測 1.387

1ステップ目の値(最後の例の 0.709)も、48ステップ後まで覚えて足せています。

サイン波の予測では、RNN と LSTM はほぼ同じ

長い記憶が要らないサイン波の課題(直前20個から次の1個)では、結果が変わりました。

モデル NumPy のテスト MSE Keras のテスト MSE
直前の値をそのまま答える 0.05759 0.05759
RNN(SimpleRNN) 0.00428 0.00386
GRU — 0.00379
LSTM 0.00393 0.00365

どのモデルも 0.004 前後で、差はわずかでした。 足した雑音(標準偏差0.05、分散0.0025)の分は、どのモデルでも当てられません。LSTM の学習時間は NumPy で RNN の約4.8倍(6.1 秒と 29.0 秒)でした。直近の値だけで答えが決まる時系列なら RNN でも同じくらい当てられ、LSTM の強みが出るのは、離れた時刻の情報が要る場合です。

LSTMとGRUの違い

この節では、LSTM を簡単にした GRU との違いと、どちらを選ぶかの目安が分かります。

GRU(Gated Recurrent Unit)は、2014年に Cho らが提案した、ゲートを2つに減らした LSTM の仲間です。 セル状態 c を持たず、隠れ状態 h だけで記憶します。PyTorch 2.14 のドキュメントの式は次のとおりです。

r_t = σ( W_ir・x_t + b_ir + W_hr・h_{t−1} + b_hr )        リセットゲート:前の h をどれだけ使って候補を作るか
z_t = σ( W_iz・x_t + b_iz + W_hz・h_{t−1} + b_hz )        更新ゲート:前の h をどれだけ残すか
n_t = tanh( W_in・x_t + b_in + r_t ⊙ ( W_hn・h_{t−1} + b_hn ) )  候補
h_t = (1 − z_t) ⊙ n_t + z_t ⊙ h_{t−1}

LSTM の「忘却ゲート f」と「入力ゲート i」を、GRU は1つの更新ゲート z で「残す割合 z、足す割合 1−z」とまとめています。

比べる点 LSTM GRU
提案 1997年(忘却ゲートは1999年に追加) 2014年
ゲート 3つ(忘却・入力・出力) 2つ(リセット・更新)
状態 セル状態 c と隠れ状態 h 隠れ状態 h だけ
重みの数(D=2・H=32) 4,480(Keras) 3,456(Keras の既定)
長さ100の記憶課題(この記事) 3回とも 0.0004 以下 3回とも 0.0002 以下
学習時間(この記事・Keras) 長さ50で 34.0 秒 長さ50で 23.7 秒

GRU と LSTM を比べた Chung らの論文(2014年)は、音楽と音声のデータで、ゲートのある LSTM・GRU はどちらも tanh だけの RNN より成績がよく、GRU は LSTM と同じくらいだったと報告しています。どちらがよいかはデータで変わるので、迷ったら両方を同じ条件で学習させ、検証データの誤差で選びます。 重みを少なくしたい・学習を速くしたいときは GRU、長い記憶が要る課題で使われてきた構成に合わせたいときは LSTM、という選び方がよく使われます。

LSTMとTransformerの違いと、今どこで使われているか

この節では、文章を扱うモデルの主流になった Transformer と LSTM の違い、LSTM の歴史と今の使われ方が分かります。

1つずつ読むか、全部を同時に見比べるか

LSTM は時系列を1ステップずつ順に読み、Transformer は Attention で全部の位置を一度に見比べます。 2017年の論文「Attention Is All You Need」で提案された Transformer は、離れた2語を1回の計算で直接つなげ、全部の位置を並べて同時に計算できます。GPT・BERT など、文章を扱う大きなモデルの多くはこの構造を土台にしています。

比べる点 LSTM Transformer
読み方 1ステップずつ順番に 全部の位置を同時に
離れた2語のつながり 途中の状態を順に通る Attention で直接つながる
学習の並列化 時刻の方向には並べにくい 並べやすい(GPU を使い切りやすい)
長さ n に対する計算 n に比例 Attention は n の2乗に比例
1つ入力が増えたときの計算 状態を1回更新するだけ 前の全部と見比べる
状態の大きさ 固定(h と c) 長さとともに増える(KV キャッシュ)

Attention の計算(Query・Key・Value)と、計算量が長さの2乗で増える様子は、関連記事「ニューラルネットワークとは|仕組み・学習・AttentionをNumPyで実演」で扱っています。Transformer を使ったモデルを Python から呼ぶ方法は、関連記事「Hugging Face transformers入門|pipelineとタスク一覧」で扱っています。

LSTM の歴史

1991年

出来事
Hochreiter が卒業論文で、RNN の勾配消失を分析

1997年

出来事
Hochreiter と Schmidhuber が論文誌 Neural Computation で LSTM を発表(1995年に技術報告)

1999年

出来事
Gers・Schmidhuber・Cummins が忘却ゲートを追加。今の LSTM の形になる

2014年

出来事
Cho らが GRU を提案

2015〜2016年

出来事
Google の音声認識(Google Voice)、Google 翻訳(GNMT)で LSTM が使われる

2017年

出来事
Transformer の論文。文章のモデルの主流が移っていく

2018〜2019年

出来事
OpenAI の Dota 2 の AI、DeepMind の AlphaStar(StarCraft II)に LSTM が使われる

2024年

出来事
Hochreiter らのチームが xLSTM を発表

英語版 Wikipedia は、ほかに Apple の QuickType と Siri、Amazon Alexa の音声を作る Polly(双方向 LSTM)、Facebook の自動翻訳での利用を挙げています。OpenAI と DeepMind の例は、強化学習(試行錯誤で報酬を最大にする学習)で LSTM を学習させたものです。強化学習の仕組みは、関連記事「強化学習とは|仕組み・Q学習・RLHFをPythonの迷路で実演」で扱っています。

今 LSTM が向いている場面

LSTM は、PyTorch(torch.nn.LSTM)・Keras(keras.layers.LSTM)の標準の層として今も用意されています。仕組みから見て、次のような場面では LSTM・GRU が選ばれやすくなります。

  • 入力が1つずつ届き、その場で答える処理:センサーの値や音声を流しながら処理する場合、LSTM は状態 h・c を1回更新するだけで次の答えを出せ、使うメモリも増えません。
  • データが少ない・小さなモデルにしたい時系列:この記事の LSTM は重み 4,513 個で、長さ100の記憶課題を解けました。
  • 比べる基準(ベースライン)として:新しいモデルを評価するとき、LSTM・GRU と同じデータで比べる。

xLSTM(2024年) は、LSTM のゲートを指数関数にして正規化を足し(exponential gating)、スカラーの記憶を持つ sLSTM と、行列の記憶を持ち並列に計算できる mLSTM を組み合わせたものです。論文の要旨は、Transformer や状態空間モデルと比べて、性能とスケーリングの両方で好ましい結果だったとしています。

LSTMの入力の形(バッチ・時系列長・特徴量)

この節では、LSTM に渡すデータの形と、1本の時系列をその形に切り出す方法が分かります。

LSTM の入力は (バッチ, 時系列長, 特徴量) の3次元です。 Keras の公式ドキュメントは、入力を「形が (batch, timesteps, feature) の3次元テンソル」と書いています。PyTorch は既定が (時系列長, バッチ, 特徴量) の順で、batch_first=True を付けると Keras と同じ順になります。

軸 意味 例:過去30日の気温から明日を予測
バッチ(N) 一度に流すサンプルの数 切り出した 335 本の窓
時系列長(T・L) 1サンプルの時刻の数 30(日)
特徴量(D・H_in) 1時刻あたりの値の数 1(気温だけ)。湿度も入れれば 2

1本の時系列(365日分の値)を、「過去30個 → 次の1個」の形に切り出して LSTM に通しました。

import numpy as np
from numpy.lib.stride_tricks import sliding_window_view
from nets import LSTM

series = np.sin(np.arange(365) * 2 * np.pi / 30)    # 1日1個の値が365日分
print("元の時系列:", series.shape)
T = 30
win = sliding_window_view(series, T + 1)            # 31個ずつの窓を1つずつずらして作る
X = win[:, :T]; y = win[:, T:]
print("切り出した X:", X.shape, " y:", y.shape)
X3 = X[:, :, None]                                  # 特徴量の軸を足す(reshape(-1, T, 1) と同じ)
print("LSTM に渡す X:", X3.shape, "= (バッチ, 時系列長, 特徴量)")
m = LSTM(D=1, H=16, seed=390)
out = m.forward(X3)
print("出力:", out.shape, " 最後の h:", m.hs[-1].shape, " 最後の c:", m.cs[-1].shape)
print("重み Wx:", m.p["Wx"].shape, " Wh:", m.p["Wh"].shape, " b:", m.p["b"].shape)
元の時系列: (365,)
切り出した X: (335, 30)  y: (335, 1)
LSTM に渡す X: (335, 30, 1) = (バッチ, 時系列長, 特徴量)
出力: (335, 1)  最後の h: (335, 16)  最後の c: (335, 16)
重み Wx: (1, 64)  Wh: (16, 64)  b: (64,)

特徴量が1つの時系列は、切り出しただけだと (335, 30) の2次元なので、最後に長さ1の軸を足して (335, 30, 1) にします。 窓の長さ(ここでは30)が、LSTM が一度に見る過去の長さになります。予測の前には、値を学習データの平均と標準偏差でそろえる(標準化する)のが一般的です。配列の形の変え方(reshape・軸の追加)は、関連記事「NumPyの使い方 基本一覧|配列の作成・演算・形状変更」で扱っています。

出力の形:return_sequences・return_state・双方向・重ねる

Keras の LSTM 層は、引数によって出力の形が変わります。(32, 10, 8) の入力で確かめました。

import numpy as np, keras
x = np.zeros((32, 10, 8), dtype="float32")           # (バッチ, 時系列長, 特徴量)
L = keras.layers
print("LSTM(4)                         ", tuple(L.LSTM(4)(x).shape))
print("LSTM(4, return_sequences=True)  ", tuple(L.LSTM(4, return_sequences=True)(x).shape))
o, h, c = L.LSTM(4, return_state=True)(x)
print("LSTM(4, return_state=True)      ", tuple(o.shape), tuple(h.shape), tuple(c.shape), "o と h は同じ:", bool(np.allclose(o, h)))
print("Bidirectional(LSTM(4))          ", tuple(L.Bidirectional(L.LSTM(4))(x).shape))
m = keras.Sequential([keras.Input((10, 8)), L.LSTM(16, return_sequences=True), L.LSTM(4)])
print("LSTM(16, return_sequences=True) → LSTM(4)", tuple(m(x).shape))
LSTM(4)                          (32, 4)
LSTM(4, return_sequences=True)   (32, 10, 4)
LSTM(4, return_state=True)       (32, 4) (32, 4) (32, 4) o と h は同じ: True
Bidirectional(LSTM(4))           (32, 8)
LSTM(16, return_sequences=True) → LSTM(4) (32, 4)
書き方 出力の形 使う場面
LSTM(4)(既定) (バッチ, 4):最後の時刻の h だけ 系列全体から1つ答える(分類・次の値)
return_sequences=True (バッチ, 時系列長, 4):全時刻の h 時刻ごとに答える・LSTM を重ねる
return_state=True 出力・最後の h・最後の c 状態を次の処理(翻訳の後半など)に渡す
Bidirectional(LSTM(4)) (バッチ, 8):前向きと後ろ向きをつなぐ 系列全体を読んでから答えられる処理

双方向 LSTM(Bidirectional LSTM)は、前から読む LSTM と後ろから読む LSTM を並べて、出力をつなげたものです。文の分類のように系列の最後まで見てから答える処理に使い、次の値を先に知ることのできない予測には使いません。PyTorch では nn.LSTM(..., bidirectional=True) で同じ形になり、出力の最後の軸が 2×hidden_size になります(公式ドキュメント)。

  • 1入力は3次元か特徴量が1つでも (N, T, 1)。x[:, :, None] か reshape(-1, T, 1) で軸を足す
  • 2軸の順番は合っているかKeras は (N, T, D)。PyTorch は既定が (T, N, D)、batch_first=True で (N, T, D)
  • 3特徴量の数が層の設定と同じかKeras は Input の形、PyTorch は input_size
  • 4LSTM を重ねるとき、前の層は return_sequences=True か付けないと2次元が次の LSTM に渡ってエラーになる
  • 5小数の型はそろっているかNumPy の既定は float64。PyTorch の重みは float32

LSTMをPythonで書く(Keras・PyTorch)

この節では、同じモデル(LSTM 32個 → 全結合1個)を Keras と PyTorch で書いた形を見比べます。

Keras

この記事で実行した形です。model.summary() の出力も載せます。

import numpy as np, keras
T = 50
xtr, ytr = adding(32000, T, np.random.default_rng(390))   # (32000, 50, 2) と (32000, 1)
model = keras.Sequential([keras.Input((T, 2)), keras.layers.LSTM(32), keras.layers.Dense(1)])
model.summary()
model.compile(optimizer=keras.optimizers.Adam(0.005, clipnorm=1.0), loss="mse")
model.fit(xtr, ytr, batch_size=64, epochs=4, verbose=2)
xte, yte = adding(5, T, np.random.default_rng(999))         # 答えさせる新しい5件
pred = model.predict(xte, verbose=0)
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                    ┃ Output Shape           ┃       Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ lstm (LSTM)                     │ (None, 32)             │         4,480 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense (Dense)                   │ (None, 1)              │            33 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 4,513 (17.63 KB)
 Trainable params: 4,513 (17.63 KB)
 Non-trainable params: 0 (0.00 B)
Epoch 1/4
500/500 - 3s - 6ms/step - loss: 0.1705
Epoch 2/4
500/500 - 2s - 5ms/step - loss: 0.0212
Epoch 3/4
500/500 - 3s - 5ms/step - loss: 7.7074e-04
Epoch 4/4
500/500 - 2s - 5ms/step - loss: 3.2444e-04

Keras 3 の LSTM 層の主な引数は次のとおりです(keras.io の公式ドキュメント)。

引数 既定 意味
units (必須) 隠れ状態の数 H
activation "tanh" 候補 g と出力の tanh
recurrent_activation "sigmoid" ゲートの関数
unit_forget_bias True 忘却ゲートのバイアスに1を足して初期化
dropout・recurrent_dropout 0.0 入力側・状態側のドロップアウト
return_sequences False 全時刻の出力を返すか
return_state False 最後の状態も返すか
go_backwards False 逆順に読むか
stateful False バッチをまたいで状態を引き継ぐか

公式ドキュメントによると、TensorFlow バックエンドで GPU があり、activation="tanh"・recurrent_activation="sigmoid"・recurrent_dropout=0・unroll=False・use_bias=True などの条件を満たすと、高速な cuDNN の実装が使われます。

PyTorch

PyTorch 2.14 の公式ドキュメント(torch.nn.LSTM)の引数と形に合わせて、同じモデルを書くと次のようになります。

import numpy as np
import torch
from torch import nn

class Net(nn.Module):
    def __init__(self, input_size=2, hidden_size=32):
        super().__init__()
        self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)   # 入力を (N, L, H_in) で受ける
        self.fc = nn.Linear(hidden_size, 1)

    def forward(self, x):                   # x: (N, L, 2)
        out, (h_n, c_n) = self.lstm(x)      # out: (N, L, 32)、h_n と c_n: (1, N, 32)
        return self.fc(out[:, -1])          # 最後の時刻の出力 → (N, 1)

model = Net()
opt = torch.optim.Adam(model.parameters(), lr=0.005)
loss_fn = nn.MSELoss()
rng = np.random.default_rng(390)
for step in range(4000):
    x, y = adding(64, 50, rng)                          # NumPy の float64
    xb = torch.tensor(x, dtype=torch.float32)            # 重みと同じ float32 にする
    yb = torch.tensor(y, dtype=torch.float32)
    loss = loss_fn(model(xb), yb)
    opt.zero_grad(); loss.backward()
    nn.utils.clip_grad_norm_(model.parameters(), 1.0)    # 勾配の大きさを1までに切る
    opt.step()
引数・値 既定・形(公式ドキュメント) 注意
batch_first False:入力は (L, N, H_in) True で (N, L, H_in)。h_0・c_0 の形は変わらない
num_layers 1 2以上で LSTM を重ねる
bidirectional False True で出力の最後の軸が 2×hidden_size
proj_size 0 正の値で h を射影して小さくする
出力 output, (h_n, c_n) h_n・c_n は (D×num_layers, N, H)
重み weight_ih_l0 (4×hidden_size, input_size) ゲートの並びは i・f・g・o

PyTorch の batch_first は、入力と出力の軸の順番だけを変え、隠れ状態 h_0・c_0 の形(層の数, バッチ, H)には効きません(公式ドキュメント)。PyTorch はバイアスを入力側・状態側の2組持つので、同じ大きさの LSTM の重みは Keras より 4×H 多くなります(D=2・H=32 で 4,608)。

形が合わないときのエラーと直し方

この節では、LSTM でいちばん多い「形が合わない」エラーの原文と、原因・直し方が分かります。

Keras の LSTM に、形の違う入力を渡しました(モデルは Input((7, 3)) で作り、正しい形は (N, 7, 3))。

model.predict に (4, 7) の2次元:
ValueError: Invalid input shape for input Traced<ShapedArray(float32[4,7])>with<DynamicJaxprTrace(level=1/0)> with name 'keras_tensor_9' and path ''. Expected shape (None, 7, 3), but input has incompatible shape (4, 7)

model.predict に特徴量が1つの (4, 7, 1):
TypeError: Exception encountered when calling LSTMCell.call().
dot_general requires contracting dimensions to have the same shape, got (1,) and (3,).
Arguments received by LSTMCell.call():
  • inputs=jnp.ndarray(shape=(4, 1), dtype=float32)
  • states=['jnp.ndarray(shape=(4, 5), dtype=float32)', 'jnp.ndarray(shape=(4, 5), dtype=float32)']
  • training=False

LSTM 層に (4, 7) の2次元を直接渡す:
ValueError: Input 0 with name 'None' of layer 'lstm_6' is incompatible with the layer: expected ndim=3, found ndim=2. Full shape received: (4, 7)

return_sequences を付けずに LSTM を重ねる:
ValueError: Input 0 with name 'None' of layer 'lstm_7' is incompatible with the layer: expected ndim=3, found ndim=2. Full shape received: (None, 16)

NumPy で書いた LSTM に、2次元の入力と、特徴量の数が違う入力を渡したときのエラーです。

[2次元のまま (335, 30)] ValueError: not enough values to unpack (expected 3, got 2)
[特徴量が3つ (335, 30, 3)] ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 1 is different from 3)

PyTorch の nn.LSTM が出すエラー文は、ソース(torch/nn/modules/rnn.py)に次の形で書かれています({ } の中に実際の値が入る)。

RuntimeError: input.size(-1) must be equal to input_size. Expected {input_size}, got {input.size(-1)}
ValueError: LSTM: Expected input to be 2D or 3D, got {input.dim()}D instead
ValueError: RNN input dtype ({input.dtype}) does not match weight dtype ({weight.dtype}). Convert input: input.to({weight.dtype}), or convert model: model.to({input.dtype})
RuntimeError: For batched 3-D input, hx and cx should also be 3-D but got ({hx[0].dim()}-D, {hx[1].dim()}-D) tensors
RuntimeError: Expected hidden[0] size {期待する形}, got {渡した形}
エラー文(要点) 原因 直し方
Keras:expected ndim=3, found ndim=2 2次元 (N, T) のまま渡した x[:, :, None] で (N, T, 1) にする
Keras:Expected shape (None, 7, 3), but input has incompatible shape Input の形と入力の形が違う Input の形か、データの切り出し方を直す
Keras:dot_general requires contracting dimensions to have the same shape 特徴量の数が、作ったときと違う Input((T, 特徴量)) の特徴量を合わせる
Keras:重ねた2つ目の LSTM で found ndim=2 前の LSTM が最後の時刻だけ返した 前の層に return_sequences=True
PyTorch:input.size(-1) must be equal to input_size 最後の軸(特徴量)が input_size と違う 軸の順番か input_size を直す
PyTorch:Expected input to be 2D or 3D, got 4D 余分な軸がある squeeze で長さ1の軸を消す
PyTorch:RNN input dtype (torch.float64) does not match weight dtype NumPy の float64 をそのまま渡した torch.tensor(x, dtype=torch.float32)
PyTorch:Expected hidden[0] size h_0 の形が (層の数, バッチ, H) でない batch_first でも h_0 は (層, N, H)
NumPy:not enough values to unpack (expected 3, got 2) 2次元を渡した 3次元にする

エラーにならない間違い

形の間違いの中には、エラーにならずに学習が進んでしまうものがあります。

  • PyTorch で batch_first を付け忘れる:(64, 50, 2) を渡すと、既定の (L, N, H_in) として「長さ64・バッチ50」と読まれます。最後の軸(特徴量 2)は合っているので、エラーは出ません。
  • Keras に長さ1の余分な軸を付ける:Input((7, 3)) のモデルに (4, 7, 3, 1) を渡すと、エラーにならず (4, 1) が返りました(最後の長さ1の軸を落として受け付けた)。
  • 時刻と特徴量を取り違える:(N, 1, 30)(長さ1・特徴量30)として渡すと、LSTM は1ステップしか計算せず、時系列として扱われません。
  1. 1渡す直前に shape を表示するprint(x.shape, x.dtype)。3次元で (N, T, D) の順か、型は float32 か
  2. 2層の設定と見比べるKeras は Input の形、PyTorch は input_size と batch_first
  3. 3出力の形も表示する最後の時刻だけか全時刻か(return_sequences・out[:, -1])で次の層の入力が変わる
  4. 4小さな入力で1回だけ通すバッチ2・長さ5のゼロの配列で通してから、本物のデータに切りかえる

LSTMのメリット・デメリットと使いどころ

この節では、LSTM を選ぶかどうかを判断するための長所・短所と、状況ごとの目安が分かります。

メリット デメリット
長い間あいた依存を学べる(長さ100の記憶課題で MSE 0.0002 前後) 重みが同じ大きさの RNN の4倍
状態が固定の大きさで、1つずつ届く入力をその場で処理できる 時刻の方向に並べて計算できず、長い系列の学習は遅い(この記事で RNN の約4.6倍の時間)
PyTorch・Keras に標準の層があり、すぐ試せる 文章の大きなモデルでは Transformer が主流
勾配消失を起こしにくい 勾配爆発は起こりうる(勾配の大きさを切る clipnorm 等で防ぐ)
状況 選ぶもの 理由
直近の値だけで決まる短い時系列 RNN・GRU・LSTM のどれでも(まず簡単な基準と比べる) サイン波では差がわずかだった
離れた時刻の情報が要る時系列 LSTM か GRU 長さ50・100で RNN は学べなかった
重みを少なく・学習を速くしたい GRU 重みが LSTM の約4分の3
長い文章・大量のデータ・GPU で並列に学習 Transformer 全部の位置を同時に計算できる
入力が流れてきて、その場で答える LSTM・GRU 1入力あたりの計算とメモリが一定

時系列の予測(売上・株価など)に使うときは、「直前の値をそのまま答える」「過去の平均を答える」といった簡単な予測と、同じテストデータで誤差を比べます。 この記事のサイン波では、直前の値を答えると MSE 0.0576、LSTM は 0.0039 で、LSTM が上回りました。簡単な予測を上回れない場合、モデルはデータから役に立つ規則を学べていません。統計の時系列モデル(ARIMA など)も、同じように比べる候補になります。

学習では、損失(MSE や交差エントロピー)が下がるように重みを動かします。分類に使うときの交差エントロピー損失の意味は、関連記事「エントロピーとは|意味・計算・交差エントロピーをPythonで実演」で扱っています。RNN の勾配消失や Transformer は E 資格の学習でも扱う内容で、試験の範囲と学ぶ順番は関連記事「E資格とは|試験内容・合格率・G検定との違いとNumPyで解く要点」で扱っています。

よくある質問

Q. LSTMとは簡単に言うと何ですか? A. 時系列や文章を1ステップずつ読みながら、3つのゲートで「何を覚え、何を忘れるか」を学ぶニューラルネットワークです。記憶専用のセル状態を持つので、ふつうの RNN より長い間あいた情報を使えます。

Q. LSTMの読み方と正式名称は? A. 「エルエスティーエム」と読みます。正式名は Long Short-Term Memory で、日本語では「長・短期記憶」と訳されます。

Q. RNNとLSTMの違いは? A. RNN は記憶が h の1本だけで、毎ステップ作り直すので遠い過去の情報と勾配が消えやすくなります。LSTM はセル状態 c とゲートを持ち、f を掛けて足すだけで記憶を運びます。この記事の長さ100の記憶課題では、RNN の MSE は 0.17 前後(何も覚えないのと同じ)、LSTM は 0.0002 前後でした。

Q. LSTMを考えたのは誰ですか? A. Sepp Hochreiter と Jürgen Schmidhuber です。1997年に論文誌 Neural Computation で発表しました。今の形に欠かせない忘却ゲートは、1999年に Gers・Schmidhuber・Cummins が足しました。

Q. LSTMの3つのゲートとは? A. 忘却ゲート(前の記憶を残す割合)、入力ゲート(新しい情報を足す割合)、出力ゲート(記憶を外に見せる割合)です。どれもシグモイド関数で 0〜1 の値を作り、掛け算で量を調節します。

Q. 勾配消失とは何ですか? A. 誤差逆伝播で、時刻をさかのぼるたびに1より小さい値が掛かり、遠い時刻に届く学習の信号がほぼ0になることです。この記事の RNN では、100ステップ前の入力への勾配が 6.51e-22 でした。

Q. LSTMとGRUはどちらを使えばよいですか? A. GRU はゲートが2つで重みが約4分の3、学習も速めです。この記事の記憶課題では、どちらも同じくらいの誤差まで下がりました。データによって変わるので、両方を同じ条件で学習させて検証データの誤差で選びます。

Q. LSTMとTransformerの違いは? A. LSTM は1ステップずつ順に読み、Transformer は Attention で全部の位置を同時に見比べます。Transformer は並列に学習しやすく文章の大きなモデルの主流で、LSTM は状態の大きさが一定で、流れてくる入力をその場で処理するのに向きます。

Q. LSTMは今も使われていますか? A. PyTorch・Keras の標準の層として用意されていて、時系列の予測や、比べる基準のモデルとして使われます。2024年には、LSTM を拡張した xLSTM も発表されています。

Q. LSTMの入力の形は? A. (バッチ, 時系列長, 特徴量) の3次元です。特徴量が1つの時系列でも (N, T, 1) にします。PyTorch は既定が (時系列長, バッチ, 特徴量) で、batch_first=True で Keras と同じ順になります。

Q. LSTMで株価予測はできますか? A. 値の列を入力すればモデルは作れますが、予測が役に立つかは別です。「前日の値をそのまま答える」などの簡単な予測と同じテストデータで比べ、上回るかを確かめます。

Q. LSTMのパラメーター数はどう計算しますか? A. 入力の特徴量 D、隠れ状態 H のとき、4 ×{H×(D+H)+H}です。D=2・H=32 なら 4,480 で、Keras の model.summary() と一致しました。PyTorch はバイアスが2組なので 4H 多くなります。

まとめ

  • LSTM は、セル状態 c と3つのゲート(忘却・入力・出力)を持つ RNN。日本語は「長・短期記憶」。
  • RNN は記憶 h を毎ステップ作り直すので、遠い過去への勾配が消える(長さ100で 6.51e-22)。
  • LSTM の c は「f を掛けて i×g を足す」だけで渡るので、f≈1 なら情報も勾配も届く。学習後の LSTM は、合図のときだけ i が開き、f が 0.976 で覚え続けるユニットを作っていた。
  • 足し算の記憶課題(長さ100)では、RNN は MSE 0.17 前後で学べず、LSTM と GRU は 0.0004 以下まで下がった。サイン波のように直近で決まる課題では差はわずか。
  • GRU はゲート2つで軽い。Transformer は全部の位置を同時に見比べ、文章の大きなモデルの主流。
  • 入力は (バッチ, 時系列長, 特徴量) の3次元。PyTorch は batch_first、Keras は return_sequences の扱いに注意する。

参考・出典

確認日はいずれも 2026年10月23日です。

  • PyTorch 2.14「torch.nn.LSTM」:https://docs.pytorch.org/docs/2.14/generated/torch.nn.LSTM.html
  • PyTorch 2.14「torch.nn.GRU」:https://docs.pytorch.org/docs/2.14/generated/torch.nn.GRU.html
  • PyTorch のソース「torch/nn/modules/rnn.py」:https://github.com/pytorch/pytorch/blob/main/torch/nn/modules/rnn.py
  • Keras 3「LSTM layer」:https://keras.io/api/layers/recurrent_layers/lstm/
  • Wikipedia「Long short-term memory」:https://en.wikipedia.org/wiki/Long_short-term_memory
  • Hochreiter, S., Schmidhuber, J.「Long Short-Term Memory」Neural Computation 9(8), 1997:https://doi.org/10.1162/neco.1997.9.8.1735
  • Chung, J. ほか「Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling」2014:https://arxiv.org/abs/1412.3555
  • Vaswani, A. ほか「Attention Is All You Need」2017:https://arxiv.org/abs/1706.03762
  • Beck, M. ほか「xLSTM: Extended Long Short-Term Memory」2024:https://arxiv.org/abs/2405.04517
  • @IT「LSTM(Long Short-Term Memory:長・短期記憶)とは?」:https://atmarkit.itmedia.co.jp/ait/articles/1901/06/news043.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「LSTMとは|仕組み・3つのゲート・RNNとの違いをPythonで実演」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?