Lesson 9 / 12

系列・Transformerの読解

このレッスンで学ぶこと

  • RNN/LSTM の入出力 shape (batch, seq, feature) を読める
  • Self-Attention の式 softmax(QKᵀ/√d)·V をコードでたどれる
  • なぜ √d で割るのか説明できる
  • Multi-Head Attention(embed_dimとヘッド分割)と位置符号の役割が分かる
  • nn.TransformerEncoder で系列分類パイプラインを組める

1. 系列データと RNN / LSTM

📘 前提:このレッスンはRNN/LSTM・Attention の理論を既習として、PyTorchでの実装と「形の追い方」に集中します。 理論があいまいなら、先に E資格対策ページ 「リカレントニューラルネットワーク ― ゲート機構(LSTM/GRU)」 に目を通すとスムーズです。

文章・音声・時系列のような順番のあるデータを扱うのが RNN(再帰型ニューラルネット)。 前の時刻の隠れ状態を次の時刻へ受け渡しながら処理します。 素のRNNは長い系列で勾配が消えやすいため、ゲート機構を持つ LSTMGRU が使われます。

PyTorch の nn.LSTM は、入力を (batch, seq, feature)batch_first=True のとき)で受け取ります。 この3次元の形を読めることが第一歩です。

2. LSTM の入出力 shape

nn.LSTM(input_size, hidden_size)。出力は2つ:各時刻の出力 out と、最後の隠れ状態 (h, c) です。

lstm.py(PyTorch・読むだけ)
import torch
import torch.nn as nn
torch.manual_seed(0)

rnn = nn.LSTM(input_size=10, hidden_size=20, batch_first=True)
x = torch.randn(4, 7, 10)   # (batch=4, seq=7, feature=10)

out, (h, c) = rnn(x)
print("out:", out.shape)   # 各時刻の出力 (4, 7, 20)
print("h  :", h.shape)     # 最後の隠れ状態 (層数=1, batch=4, hidden=20)
print("c  :", c.shape)     # セル状態 (1, 4, 20)
▼ 出力
out: torch.Size([4, 7, 20])
h  : torch.Size([1, 4, 20])
c  : torch.Size([1, 4, 20])

式だけだとイメージしづらいので、LSTM を時間方向に展開(unroll)した図で outh の位置を確認します。

LSTMを時系列方向に展開した図。入力(batch=4, seq=7, feature=10)を各時刻のLSTMセルが処理し、各時刻の隠れ状態 h_t が out(長さ7の系列)として出力され、最終時刻の隠れ状態 h とセル状態 c が別に取り出される
LSTM を時間方向に展開した図。各時刻の隠れ状態 h₁〜h₇ が out(全時刻ぶん)、最終時刻の h₇・c₇ が hc として取り出される。入力 (batch=4, seq=7, feature=10) → out (4, 7, 20)、hc (1, 4, 20)。

💡 out と h の違い(上の図のとおり): out全時刻ぶんの隠れ状態 (batch, seq, hidden)=図の h1〜h7 を並べたもの。 h最後の時刻だけ (層数, batch, hidden)=図の h7。 分類タスクでは系列を1つに集約する必要があるので、最後の時刻 out[:, -1]h[-1] を全結合に渡すのが定番です。

押さえどころは out最後の次元の入れ替わりです。最後の次元は、入力の feature(ここでは10)から hidden(ここでは20)に置き換わります—— 「10 がそのまま hidden になる」のではなく、別サイズの 20 になる点に注意。batchseq は変わりません:(4, 7, 10) → (4, 7, 20)

3. Self-Attention(Transformer の核)

Transformer は RNN のように順番に処理せず、系列の全要素どうしの関連を一度に見ます。その仕組みが Self-Attention。 各要素から Q(クエリ)・K(キー)・V(バリュー)を作り、QとKの内積で「どの要素に注目するか」の重みを決め、Vを加重平均します。

Self-Attentionの図:QとKの内積をsqrt(d)で割りsoftmaxし、Vと加重和を取る
Q·Kᵀ で関連度を測り、√dで割って softmax → その重みで V を加重平均する
self_attention.py(PyTorch・読むだけ)
import torch
import torch.nn.functional as F
import math
torch.manual_seed(0)

seq, d = 3, 4               # 系列長3, 次元4
Q = torch.randn(seq, d)
K = torch.randn(seq, d)
V = torch.randn(seq, d)

scores = Q @ K.T / math.sqrt(d)   # 関連度 (3, 3)。√dで割る
weights = F.softmax(scores, dim=-1) # 各行を確率に (3, 3)
ctx = weights @ V                  # Vの加重平均 (3, 4)

print("scores :", scores.shape)         # (3, 3)
print("weights:", weights.shape)        # (3, 3)
print("出力   :", ctx.shape)            # (3, 4)
print("各行の重みの合計:", weights.sum(dim=-1).tolist())  # [1.0, 1.0, 1.0]
▼ 出力
scores : torch.Size([3, 3])
weights: torch.Size([3, 3])
出力   : torch.Size([3, 4])
各行の重みの合計: [1.0, 1.0, 1.0]

⚠ なぜ √d で割るのか(つまずきやすい点): 次元 d が大きいと内積 QKᵀ の値が大きくなりがちで、softmax が極端(ほぼ0か1)になり勾配が消えます。 √d で割ってスケールを抑えることで、softmax がなだらかになり学習が安定します。これを スケール化ドット積注意(Scaled Dot-Product Attention)と呼びます。

📘 理論の復習: Self-Attention(Scaled Dot-Product Attention)の式の意味や導出は、E資格対策ページ 「Transformer ― Scaled Dot-Product Attention」 で復習できます。

4. Multi-Head Attention と位置符号

実際の Transformer は、Attention を複数の「ヘッド」に分けて並列に行います(Multi-Head Attention)。 入力の各トークンは長さ embed_dim のベクトルで表され、それを num_heads 個に等分して、各ヘッドが head_dim = embed_dim / num_heads 次元だけを担当します。 各ヘッドが異なる観点の関連を捉え、最後に結合して元の embed_dim に戻します。PyTorch には nn.MultiheadAttention があります。

multihead.py(PyTorch・読むだけ)
import torch
import torch.nn as nn
torch.manual_seed(0)

mha = nn.MultiheadAttention(embed_dim=16, num_heads=4, batch_first=True)  # 16次元を4ヘッドに分割(各ヘッド head_dim=4)
x = torch.randn(2, 5, 16)   # (batch=2, seq=5, embed_dim=16)

# Self-Attentionなので Q=K=V=x を渡す
out, attn = mha(x, x, x)
print("出力        :", out.shape)    # (2, 5, 16) 形は保たれる
print("注意の重み  :", attn.shape)   # (2, 5, 5) 各要素が各要素にどれだけ注目したか
▼ 出力
出力        : torch.Size([2, 5, 16])
注意の重み  : torch.Size([2, 5, 5])

💡 コードの読み方(Multi-Head Attention):

  • embed_dim=16 … 1トークンを表すベクトルの長さ。入力 x(batch=2, seq=5, embed=16)
  • num_heads=4 … ヘッド数。16 ÷ 4 = 4 次元ずつを4つのヘッドが分担(head_dim=4)。
  • out, attn = mha(x, x, x) … Self-Attention なので Q・K・V に同じ x を渡す。
  • out … 各トークンを「他のトークンの情報で文脈化」した結果。形は入力と同じ (2, 5, 16)(だから層を何段も積める)。
  • attn注意の重み (2, 5, 5)(batch, seq, seq)。「どのトークンが、どのトークンにどれだけ注目したか」の 5×5 の表(既定では全ヘッドの平均)。

👀 attn の3つ目の次元(=5)はどこから? 注目する側(クエリ)が seq=5 個、注目される側(キー)も同じ系列の seq=5なので、組み合わせは 5×5。これにバッチをつけて (batch=2, 5, 5) です。 attn[b, i, j] は「バッチ b で、トークン i がトークン j にどれだけ注目したか」を表し、各行(i を固定)は softmax なので合計1になります。

実装での使い方:ふつうは Transformer ブロックの中で使い、out をそのまま次の層へ流します。1ブロックを自前で組むより、まとめ役の nn.TransformerEncoderLayer を使うのが実務では定番です(§5で使用)。

embed_dimnum_heads で割り切れること: 各ヘッドは embed_dim を等分した head_dim = embed_dim / num_heads 次元を担当します。割り切れないと等分できずエラーになります (例:embed_dim=16, num_heads=4 → head_dim=4 はOK/num_heads=316/3 が割り切れずNG)。

位置符号(Positional Encoding)とは: Self-Attention は要素を「集合」として扱い、順番の情報を持ちません。 そこで各位置に固有のベクトル(位置符号)を入力に足して、語順を伝えます(§5では学習可能な位置符号を使います)。
📘 理論の復習: Multi-Head Attention・位置符号の詳細は、E資格対策ページ 「Transformer ― Multi-Head Attention / Positional Encoding」 で復習できます。

5. 総合演習:Transformer で系列分類

仕上げに、このレッスンの部品——位置符号・Multi-Head Self-Attention(nn.TransformerEncoder の中身)・系列の集約・学習/評価——を1つにまとめた 系列分類パイプラインを読み解きます。各時刻の特徴ベクトルを Transformer で文脈化し、系列全体を平均してクラスを当てます。

🔥 ここが最大の山場! 少し長い、これまでの集大成のコードです。最初は「うっ…」となって当然——でも大丈夫、コメントを1行ずつ追えば必ず読めますこのコードを最後まで自力で追えたら、Transformer の実装はもう十分に理解できています。胸を張ってOKです! 細かい数式より「どの部品が・どの順で・どんな形のデータを流すか」を掴むのがゴールです。

seq_classifier.py(PyTorch・読むだけ)
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
torch.manual_seed(0)

# ============================================================
# 1) データを用意する
#    長さ12・各時刻8次元の系列を 240本 作り、2クラスに分ける。
#    ラベル y =「系列全体の平均値が正なら 1、そうでなければ 0」
#    → 系列をうまく集約できれば解ける、素直なタスク
# ============================================================
N, seq, feat = 240, 12, 8
X = torch.randn(N, seq, feat)              # (240, 12, 8) = (本数, 系列長, 特徴)
y = (X.mean(dim=(1, 2)) > 0).long()        # 各系列の全要素平均>0 → 0/1 ラベル (240,)
train = DataLoader(TensorDataset(X[:200], y[:200]), batch_size=32, shuffle=True)  # 学習用200本
test  = DataLoader(TensorDataset(X[200:], y[200:]), batch_size=32)                # 評価用 40本

# ============================================================
# 2) モデルを定義する
#    各時刻の特徴ベクトルを Transformer で「文脈化」し、
#    系列全体を1つにまとめて 2クラスを予測する
# ============================================================
d_model = 16                               # Transformer内部で1トークンを表すベクトル長
class SeqClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        # 入力の特徴8次元 → d_model=16 へ変換(Transformerが扱う次元にそろえる)
        self.proj = nn.Linear(feat, d_model)

        # --- 位置符号(Positional Encoding)-------------------------------
        # Attentionは「順番」を見ないので、"何番目か" の情報を自分で足す必要がある。
        # ここでは各位置に専用ベクトルを持たせ、学習で最適化する方式(=学習可能な位置符号)。
        # 形 (1, seq, d_model) = (1, 12, 16):
        #   ・先頭の 1 は「どのバッチでも同じ位置符号を使う」という意味。
        #     足し算のとき batch 方向へ自動コピー(ブロードキャスト)される。
        #   ・nn.Parameter にすると、この位置符号自体も学習対象になる。
        self.pos  = nn.Parameter(torch.randn(1, seq, d_model))

        # --- Transformerエンコーダ本体 -----------------------------------
        # encoder_layer 1枚 =「Multi-Head Self-Attention + 全結合(FFN)」のブロック。
        #   d_model=16          : トークンのベクトル長
        #   nhead=4             : ヘッド数(16 ÷ 4 = 4 で割り切れること!)
        #   dim_feedforward=32  : ブロック内の中間全結合の幅
        #   batch_first=True    : 入力を (batch, seq, d_model) の順で渡す
        layer = nn.TransformerEncoderLayer(d_model=16, nhead=4,
                                           dim_feedforward=32, batch_first=True)
        # そのブロックを 2段 重ねる(num_layers=2)
        self.encoder = nn.TransformerEncoder(layer, num_layers=2)

        # 集約後のベクトル(16次元) → 2クラスのスコア
        self.head = nn.Linear(d_model, 2)

    def forward(self, x):                  # x : (batch, 12, 8)
        h = self.proj(x) + self.pos     # 16次元へ変換し、位置符号を足す → (batch, 12, 16)
        h = self.encoder(h)             # 各時刻を "他の時刻の情報" で文脈化 → (batch, 12, 16)
        h = h.mean(dim=1)               # 系列方向(dim=1)に平均=系列を1ベクトルに集約 → (batch, 16)
        return self.head(h)             # クラススコア → (batch, 2)

model = SeqClassifier()
loss_fn = nn.CrossEntropyLoss()            # 分類の損失(ロジットを渡す。L05参照)
opt = torch.optim.Adam(model.parameters(), lr=1e-3)

# ============================================================
# 3) 評価関数(L08と同じ作法):eval() + no_grad で正解率を測る
# ============================================================
def evaluate(model, loader):
    model.eval()                           # 評価モード(dropout等を評価用に)
    correct, total = 0, 0
    with torch.no_grad():                  # 勾配を計算しない(速い・省メモリ)
        for xb, yb in loader:
            correct += (model(xb).argmax(1) == yb).sum().item()  # 予測(argmax)が正解と一致した数を加算
            total += yb.size(0)
    return correct / total                  # 正解数 ÷ 全体

# ============================================================
# 4) 学習ループ(L06):train()で1エポック学習 → evaluate()で測る
# ============================================================
for epoch in range(5):
    model.train()                          # 学習モード
    for xb, yb in train:                   # ミニバッチごとに
        opt.zero_grad()                    # ① 前回の勾配をリセット
        loss = loss_fn(model(xb), yb)      # ② 順伝播 → 損失
        loss.backward()                    # ③ 逆伝播(勾配を計算)
        opt.step()                         # ④ パラメータ更新
    print(f"epoch {epoch}  test_acc {evaluate(model, test):.3f}")  # 1エポックごとにテスト精度
▼ 出力
epoch 0  test_acc 0.600
epoch 1  test_acc 0.625
epoch 2  test_acc 0.625
epoch 3  test_acc 0.675
epoch 4  test_acc 0.800

💡 ここを変えて試してみよう(まず出力を予想 → 手元で確かめる):

  • nhead=428 に変えると?(d_model=16 で割り切れる数にすること)
  • num_layers を増やすと精度・学習時間はどうなる?
  • 位置符号 self.pos を足すのをやめると、順番が関わるタスクでどう変わる?
  • Transformer の代わりに nn.LSTMout[:, -1] で分類器を作ると?

※ PyTorchはこのページでは動かせないので、まず頭の中で予想してから手元で実際に動かすのがおすすめ(合成タスクなので精度は参考値)。

6. 練習問題

問題 1

Attention スコアの shape は?

QK がどちらも形 (seq=5, d=8) のとき、Q @ K.T の shape はどれですか。

  • A. (8, 8)
  • B. (5, 5)
  • C. (5, 8)
答えと解説を見る

正解:B. (5, 5)

(5,8) @ (8,5) = (5,5)。系列の各要素どうしの関連度なので、行・列とも系列長 seq になります。

問題 2

なぜ √d で割る?

Self-Attention で QKᵀ√d で割る理由はどれですか。

  • A. 計算を速くするため
  • B. 出力の形を変えるため
  • C. 内積が大きくなりすぎて softmax の勾配が消えるのを防ぐため
答えと解説を見る

正解:C

次元が大きいと内積が大きくなり、softmax が極端になって勾配が小さくなります。√d で割ってスケールを抑え、学習を安定させます。

問題 3

LSTM の出力 shape は?

nn.LSTM(input_size=8, hidden_size=32, batch_first=True)(16, 10, 8) を通したときの out の shape はどれですか。

  • A. (16, 10, 32)
  • B. (16, 10, 8)
  • C. (16, 32)
答えと解説を見る

正解:A. (16, 10, 32)

batch=16・seq=10 はそのまま、feature が 8 → hidden=32 に変わります。

問題 4

embed_dim と num_heads の関係は?

nn.MultiheadAttention(embed_dim, num_heads) で成り立つべき条件はどれですか。

  • A. num_heads が embed_dim より大きい
  • B. embed_dim が num_heads で割り切れる
  • C. 両者が等しい
答えと解説を見る

正解:B

埋め込み次元をヘッド数で等分するため、embed_dimnum_heads割り切れる必要があります(例:16÷4=4)。 割り切れないとエラーになります。

問題 5

LSTM の out の shape は?

次のコードの out の shape はどれですか。

quiz_lstm.py
rnn = nn.LSTM(input_size=12, hidden_size=24, batch_first=True)
out, (h, c) = rnn(torch.randn(8, 15, 12))
  • A. (8, 15, 12)
  • B. (8, 24)
  • C. (8, 15, 24)
答えと解説を見る

正解:C. (8, 15, 24)

batch=8・seq=15 はそのまま、feature が 12 → hidden=24 に変わります。out全時刻ぶんなので3次元です。

問題 6

注意の重み attn の shape は?

次のコードの attn の shape はどれですか。

quiz_mha.py
mha = nn.MultiheadAttention(embed_dim=32, num_heads=8, batch_first=True)
x = torch.randn(4, 6, 32)
out, attn = mha(x, x, x)
  • A. (4, 6, 6)
  • B. (4, 6, 32)
  • C. (6, 6)
答えと解説を見る

正解:A. (4, 6, 6)

注意の重みは「各トークンが各トークンにどれだけ注目したか」なので (batch, seq, seq) = (4, 6, 6)out のほうは入力と同じ (4, 6, 32) です(32÷8=4 でヘッドに分割)。

7. まとめ

このレッスンのポイント

  • RNN/LSTM は順番のあるデータを隠れ状態を渡しながら処理。入力は (batch, seq, feature)
  • LSTM の out は全時刻 (batch, seq, hidden)h は最後の隠れ状態(分類は out[:, -1]h[-1]
  • Self-Attention は softmax(QKᵀ/√d)·V。スコアは (seq, seq)
  • √d で割るのは softmax を安定させ勾配消失を防ぐため
  • Multi-Head は Attention を複数ヘッドで並列実行。head_dim = embed_dim / num_heads なので embed_dim は num_heads で割り切れる
  • 位置符号(Positional Encoding)で語順を入力に足して伝える
  • 系列分類は 位置符号 → nn.TransformerEncoder → 系列を集約(平均など)→ 全結合の流れ

ここまでで深層学習の主要アーキ(全結合・CNN・系列/Transformer)の実装を読めるようになりました。 次の Unit 4 では、シラバス範囲内の機械学習(パターン認識・評価指標)を scikit-learn で実際に動かします。

完了するとコース一覧に進捗が記録されます