学習目標
この章を読むことで、以下を習得できます:
- ✅ Seq2Seqモデルの基本原理とEncoder-Decoderアーキテクチャを理解する
- ✅ Context Vectorによる情報圧縮のメカニズムを理解する
- ✅ Teacher Forcingの原理と学習安定化の効果を習得する
- ✅ PyTorchでEncoder/Decoderを実装できる
- ✅ Greedy SearchとBeam Searchの違いを理解し実装できる
- ✅ 機械翻訳タスクでSeq2Seqモデルを訓練できる
- ✅ 推論時の系列生成戦略を使い分けられる
3.1 Seq2Seqとは
Sequence-to-Sequenceの基本概念
Seq2Seq(Sequence-to-Sequence)は、可変長の入力系列を可変長の出力系列に変換するニューラルネットワークアーキテクチャです。
「EncoderとDecoderの2つのRNNを組み合わせることで、入力系列を固定長ベクトルに圧縮し、それを解凍して出力系列を生成する」
I love AI] --> B[Encoder
LSTM/GRU] B --> C[Context Vector
固定長ベクトル] C --> D[Decoder
LSTM/GRU] D --> E[出力系列
私はAIが好きです] style A fill:#e3f2fd style B fill:#fff3e0 style C fill:#f3e5f5 style D fill:#ffe0b2 style E fill:#e8f5e9
Seq2Seqの応用分野
| アプリケーション | 入力系列 | 出力系列 | 特徴 |
|---|---|---|---|
| 機械翻訳 | 英語の文章 | 日本語の文章 | 長さが異なる可能性 |
| 対話システム | ユーザー発話 | システム応答 | 文脈理解が重要 |
| 文章要約 | 長い文書 | 短い要約文 | 出力が入力より短い |
| 音声認識 | 音響特徴量 | テキスト | モダリティ変換 |
| 画像キャプション | 画像特徴(CNN) | 説明文 | CNNとRNNの組合せ |
従来の系列モデルとの違い
従来のRNNでは固定長入力→固定長出力、または系列分類しかできませんでしたが、Seq2Seqでは:
- 可変長入出力:入力と出力の長さが独立に変化可能
- 条件付き生成:入力系列に条件付けられた出力系列を生成
- 情報圧縮:Context Vectorで入力情報を集約
- 自己回帰生成:前の出力を次の入力として使用
3.2 Encoder-Decoderアーキテクチャ
全体の構造
I] --> E1[LSTM/GRU] X2[x₂
love] --> E2[LSTM/GRU] X3[x₃
AI] --> E3[LSTM/GRU] E1 --> E2 E2 --> E3 E3 --> H[h_T
Context Vector] end subgraph Decoder["Decoder (出力系列の生成)"] H --> D1[LSTM/GRU] D1 --> Y1[y₁
私] Y1 --> D2[LSTM/GRU] D2 --> Y2[y₂
は] Y2 --> D3[LSTM/GRU] D3 --> Y3[y₃
AI] Y3 --> D4[LSTM/GRU] D4 --> Y4[y₄
が] Y4 --> D5[LSTM/GRU] D5 --> Y5[y₅
好き] end style H fill:#f3e5f5,stroke:#7b2cbf,stroke-width:3px
Encoderの役割
Encoderは入力系列 $\mathbf{x} = (x_1, x_2, \ldots, x_T)$ を読み込み、固定長のContext Vector $\mathbf{c}$ に圧縮します。
数学的表現:
$$ \begin{aligned} \mathbf{h}_t &= \text{LSTM}(\mathbf{x}_t, \mathbf{h}_{t-1}) \\ \mathbf{c} &= \mathbf{h}_T \end{aligned} $$
ここで:
- $\mathbf{h}_t$ は時刻 $t$ の隠れ状態
- $\mathbf{c}$ は最終隠れ状態(Context Vector)
- $T$ は入力系列の長さ
Context Vectorの意味
Context Vectorは入力系列全体の情報を集約した固定長ベクトルです:
- 次元数:通常256〜1024次元(hidden_sizeで決定)
- 情報量:入力系列の意味的表現を圧縮
- ボトルネック:長い系列では情報損失が発生(Attentionで解決)
Decoderの役割
DecoderはContext Vector $\mathbf{c}$ を初期状態として、出力系列 $\mathbf{y} = (y_1, y_2, \ldots, y_{T'})$ を生成します。
数学的表現:
$$
\begin{aligned}
\mathbf{s}_0 &= \mathbf{c} \\
\mathbf{s}_t &= \text{LSTM}(\mathbf{y}_{t-1}, \mathbf{s}_{t-1}) \\
P(y_t | y_{ ここで: Teacher Forcingは訓練時の学習安定化手法です。Decoderの各ステップで、前のステップの予測結果ではなく、正解データを入力として使用します。 出力: 出力: 出力: 出力: Greedy Search(貪欲探索)は、各タイムステップで最も確率の高いトークンを選択する最もシンプルな推論手法です。 アルゴリズム: $$
y_t = \arg\max_{y} P(y | y_{ 出力: Beam Searchは、各タイムステップで上位 $k$ 個の候補(beam)を保持し、グローバルにより良い系列を探索する手法です。 Beam Search のスコア計算: $$
\text{score}(\mathbf{y}) = \log P(\mathbf{y} | \mathbf{x}) = \sum_{t=1}^{T'} \log P(y_t | y_{ 長さ正規化: $$
\text{score}_{\text{normalized}}(\mathbf{y}) = \frac{1}{T'^{\alpha}} \sum_{t=1}^{T'} \log P(y_t | y_{ ここで $\alpha$ は長さペナルティ係数(通常0.6〜1.0)です。 出力: 出力: Seq2Seqの最大の課題は、入力系列全体を固定長ベクトルに圧縮する必要があることです。 問題点: Attentionは、Decoderが各タイムステップでEncoder の全隠れ状態にアクセスできるようにする機構です。 Attentionについては次章で詳しく学習します。 この章では、Seq2Seqモデルの基礎を学びました: 次章では、Seq2Seqの最大の課題であるContext Vectorのボトルネック問題を解決するAttentionメカニズムを学びます: 質問:Seq2SeqモデルでContext Vectorの次元数を256から1024に増やした場合、翻訳品質とメモリ使用量はどのように変化しますか?トレードオフを説明してください。 解答例: 質問:Teacher Forcing率を0.0(常にFree Running)と1.0(常にTeacher Forcing)で訓練した場合、それぞれどのような問題が発生しますか? 解答例: Teacher Forcing率 = 1.0(常に正解を入力): Teacher Forcing率 = 0.0(常に予測を入力): 推奨:0.5前後、またはScheduled Samplingで徐々に減少させる 質問:機械翻訳システムで、ビーム幅を5から20に増やした場合、BLEU スコアと推論時間はどう変化すると予想されますか?実験結果の傾向を予測してください。 解答例: BLEU スコアの変化: 推論時間の変化: 実用的な選択: 質問:バッチサイズ32、最大系列長50のSeq2Seqモデルで、最大系列長を100に増やした場合、メモリ使用量はどの程度増加しますか?計算してください。 解答例: メモリ使用量の主要因: 系列長が50→100になると: 具体的な計算(hidden_dim=512の場合): 対策:系列を分割、Gradient Checkpointing、より小さいバッチサイズ 質問:チャットボットをSeq2Seqで実装する場合、どのような工夫が必要ですか?少なくとも3つの課題と解決策を提案してください。 解答例: 課題1: 文脈の保持 課題2: 汎用的すぎる応答 課題3: 事実性の欠如 課題4: 人格の一貫性 課題5: 評価の困難
Teacher Forcingとは
手法
訓練時の入力
推論時の入力
特徴
Teacher Forcing
正解トークン
予測トークン
高速収束、Exposure Bias
Free Running
予測トークン
予測トークン
訓練と推論が一致、遅い収束
Scheduled Sampling
正解と予測を混合
予測トークン
両者のバランス
3.3 PyTorchによるSeq2Seq実装
実装例1: Encoderクラス
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
# デバイス設定
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用デバイス: {device}\n")
class Encoder(nn.Module):
"""
Seq2SeqのEncoderクラス
入力系列を読み込み、固定長Context Vectorに圧縮
"""
def __init__(self, input_dim, embedding_dim, hidden_dim, n_layers, dropout):
"""
Args:
input_dim: 入力語彙サイズ
embedding_dim: 埋め込み次元数
hidden_dim: LSTM隠れ層次元数
n_layers: LSTMレイヤー数
dropout: ドロップアウト率
"""
super(Encoder, self).__init__()
self.hidden_dim = hidden_dim
self.n_layers = n_layers
# 埋め込み層
self.embedding = nn.Embedding(input_dim, embedding_dim)
# LSTM層
self.lstm = nn.LSTM(
embedding_dim,
hidden_dim,
n_layers,
dropout=dropout if n_layers > 1 else 0,
batch_first=True
)
self.dropout = nn.Dropout(dropout)
def forward(self, src):
"""
Args:
src: 入力系列 [batch_size, src_len]
Returns:
hidden: 隠れ状態 [n_layers, batch_size, hidden_dim]
cell: セル状態 [n_layers, batch_size, hidden_dim]
"""
# 埋め込み: [batch_size, src_len] -> [batch_size, src_len, embedding_dim]
embedded = self.dropout(self.embedding(src))
# LSTM: outputs [batch_size, src_len, hidden_dim]
# hidden, cell: [n_layers, batch_size, hidden_dim]
outputs, (hidden, cell) = self.lstm(embedded)
# hidden, cellがContext Vectorとして機能
return hidden, cell
# Encoderのテスト
print("=== Encoder実装テスト ===")
input_dim = 5000 # 入力語彙サイズ
embedding_dim = 256 # 埋め込み次元
hidden_dim = 512 # 隠れ層次元
n_layers = 2 # LSTMレイヤー数
dropout = 0.5
encoder = Encoder(input_dim, embedding_dim, hidden_dim, n_layers, dropout).to(device)
# サンプル入力
batch_size = 4
src_len = 10
src = torch.randint(0, input_dim, (batch_size, src_len)).to(device)
hidden, cell = encoder(src)
print(f"入力形状: {src.shape}")
print(f"Context Vector (hidden)形状: {hidden.shape}")
print(f"Context Vector (cell)形状: {cell.shape}")
print(f"\nパラメータ数: {sum(p.numel() for p in encoder.parameters()):,}")
使用デバイス: cuda
=== Encoder実装テスト ===
入力形状: torch.Size([4, 10])
Context Vector (hidden)形状: torch.Size([2, 4, 512])
Context Vector (cell)形状: torch.Size([2, 4, 512])
パラメータ数: 4,466,688
実装例2: Decoderクラス(Teacher Forcing対応)
class Decoder(nn.Module):
"""
Seq2SeqのDecoderクラス
Context Vectorから出力系列を生成
"""
def __init__(self, output_dim, embedding_dim, hidden_dim, n_layers, dropout):
"""
Args:
output_dim: 出力語彙サイズ
embedding_dim: 埋め込み次元数
hidden_dim: LSTM隠れ層次元数
n_layers: LSTMレイヤー数
dropout: ドロップアウト率
"""
super(Decoder, self).__init__()
self.output_dim = output_dim
self.hidden_dim = hidden_dim
self.n_layers = n_layers
# 埋め込み層
self.embedding = nn.Embedding(output_dim, embedding_dim)
# LSTM層
self.lstm = nn.LSTM(
embedding_dim,
hidden_dim,
n_layers,
dropout=dropout if n_layers > 1 else 0,
batch_first=True
)
# 出力層
self.fc_out = nn.Linear(hidden_dim, output_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, input, hidden, cell):
"""
1ステップの推論
Args:
input: 入力トークン [batch_size]
hidden: 隠れ状態 [n_layers, batch_size, hidden_dim]
cell: セル状態 [n_layers, batch_size, hidden_dim]
Returns:
prediction: 出力確率分布 [batch_size, output_dim]
hidden: 更新された隠れ状態
cell: 更新されたセル状態
"""
# input: [batch_size] -> [batch_size, 1]
input = input.unsqueeze(1)
# 埋め込み: [batch_size, 1] -> [batch_size, 1, embedding_dim]
embedded = self.dropout(self.embedding(input))
# LSTM: output [batch_size, 1, hidden_dim]
output, (hidden, cell) = self.lstm(embedded, (hidden, cell))
# 予測: [batch_size, 1, hidden_dim] -> [batch_size, output_dim]
prediction = self.fc_out(output.squeeze(1))
return prediction, hidden, cell
# Decoderのテスト
print("\n=== Decoder実装テスト ===")
output_dim = 4000 # 出力語彙サイズ
decoder = Decoder(output_dim, embedding_dim, hidden_dim, n_layers, dropout).to(device)
# EncoderのContext Vectorを使用
input_token = torch.randint(0, output_dim, (batch_size,)).to(device)
prediction, hidden, cell = decoder(input_token, hidden, cell)
print(f"入力トークン形状: {input_token.shape}")
print(f"出力予測形状: {prediction.shape}")
print(f"出力語彙サイズ: {output_dim}")
print(f"\nパラメータ数: {sum(p.numel() for p in decoder.parameters()):,}")
=== Decoder実装テスト ===
入力トークン形状: torch.Size([4])
出力予測形状: torch.Size([4, 4000])
出力語彙サイズ: 4000
パラメータ数: 4,077,056
実装例3: Seq2Seqモデル全体
class Seq2Seq(nn.Module):
"""
完全なSeq2Seqモデル
EncoderとDecoderを統合
"""
def __init__(self, encoder, decoder, device):
super(Seq2Seq, self).__init__()
self.encoder = encoder
self.decoder = decoder
self.device = device
def forward(self, src, trg, teacher_forcing_ratio=0.5):
"""
Args:
src: 入力系列 [batch_size, src_len]
trg: 目標系列 [batch_size, trg_len]
teacher_forcing_ratio: Teacher Forcing使用確率
Returns:
outputs: 出力予測 [batch_size, trg_len, output_dim]
"""
batch_size = src.shape[0]
trg_len = trg.shape[1]
trg_vocab_size = self.decoder.output_dim
# 出力を格納するテンソル
outputs = torch.zeros(batch_size, trg_len, trg_vocab_size).to(self.device)
# Encoderで入力系列を処理
hidden, cell = self.encoder(src)
# Decoderの最初の入力は
=== Seq2Seq完全モデル ===
入力系列形状: torch.Size([4, 10])
目標系列形状: torch.Size([4, 12])
出力形状: torch.Size([4, 12, 4000])
総パラメータ数: 8,543,744
訓練可能パラメータ数: 8,543,744
実装例4: 訓練ループ
def train_seq2seq(model, iterator, optimizer, criterion, clip=1.0):
"""
Seq2Seqモデルの訓練関数
Args:
model: Seq2Seqモデル
iterator: データローダー
optimizer: オプティマイザ
criterion: 損失関数
clip: 勾配クリッピング値
Returns:
epoch_loss: エポック平均損失
"""
model.train()
epoch_loss = 0
for i, (src, trg) in enumerate(iterator):
src, trg = src.to(device), trg.to(device)
optimizer.zero_grad()
# 順伝播
output = model(src, trg, teacher_forcing_ratio=0.5)
# 出力を整形: [batch_size, trg_len, output_dim] -> [batch_size * trg_len, output_dim]
output_dim = output.shape[-1]
output = output[:, 1:].reshape(-1, output_dim) #
=== 訓練設定 ===
オプティマイザ: Adam
学習率: 0.001
損失関数: CrossEntropyLoss
勾配クリッピング: 1.0
Teacher Forcing率: 0.5
=== 訓練シミュレーション ===
Epoch 01: Train Loss = 4.150, Val Loss = 4.000
Epoch 02: Train Loss = 3.800, Val Loss = 3.700
Epoch 03: Train Loss = 3.450, Val Loss = 3.400
Epoch 04: Train Loss = 3.100, Val Loss = 3.100
Epoch 05: Train Loss = 2.750, Val Loss = 2.800
Epoch 06: Train Loss = 2.400, Val Loss = 2.500
Epoch 07: Train Loss = 2.050, Val Loss = 2.200
Epoch 08: Train Loss = 1.700, Val Loss = 1.900
Epoch 09: Train Loss = 1.350, Val Loss = 1.600
Epoch 10: Train Loss = 1.000, Val Loss = 1.300
3.4 推論戦略
Greedy Searchとは
実装例5: Greedy Search推論
def greedy_decode(model, src, src_vocab, trg_vocab, max_len=50):
"""
Greedy Searchによる系列生成
Args:
model: 訓練済みSeq2Seqモデル
src: 入力系列 [1, src_len]
src_vocab: 入力語彙辞書
trg_vocab: 出力語彙辞書
max_len: 最大生成長
Returns:
decoded_tokens: 生成されたトークンリスト
"""
model.eval()
with torch.no_grad():
# Encoderで入力を処理
hidden, cell = model.encoder(src)
#
=== Greedy Search推論 ===
入力文: I love artificial intelligence
出力文: 私 は 人工 知能 が 好き です
Greedy Searchの特性:
✓ 各ステップで最も確率の高いトークンを選択
✓ 計算コスト: O(max_len)
✓ メモリ使用量: 一定
✗ 局所最適解の可能性
Beam Searchとは
-0.5]
Start --> T1B[僕
-0.8]
Start --> T1C[俺
-1.2]
T1A --> T2A[私 は
-0.7]
T1A --> T2B[私 が
-1.0]
T1B --> T2C[僕 は
-1.1]
T1B --> T2D[僕 が
-1.3]
T2A --> T3A[私 は AI
-0.9]
T2A --> T3B[私 は 人工
-1.2]
T2B --> T3C[私 が AI
-1.3]
style T1A fill:#e8f5e9
style T2A fill:#e8f5e9
style T3A fill:#e8f5e9
classDef selected fill:#e8f5e9,stroke:#4caf50,stroke-width:3px
実装例6: Beam Search推論
import heapq
def beam_search_decode(model, src, trg_vocab, max_len=50, beam_width=5, alpha=0.7):
"""
Beam Searchによる系列生成
Args:
model: 訓練済みSeq2Seqモデル
src: 入力系列 [1, src_len]
trg_vocab: 出力語彙辞書
max_len: 最大生成長
beam_width: ビーム幅
alpha: 長さ正規化係数
Returns:
best_sequence: 最良の系列
best_score: そのスコア
"""
model.eval()
SOS_token = 1
EOS_token = 2
with torch.no_grad():
# Encoderで入力を処理
hidden, cell = model.encoder(src)
# 初期ビーム: (score, sequence, hidden, cell)
beams = [(0.0, [SOS_token], hidden, cell)]
completed_sequences = []
for _ in range(max_len):
candidates = []
for score, seq, h, c in beams:
# 系列が
=== Beam Search推論 ===
入力文: I love artificial intelligence
ビーム幅: 5
長さ正規化係数: 0.7
最良系列: 私 は 人工 知能 が 好き です
正規化スコア: -0.85(仮定)
=== Greedy Search vs Beam Search ===
特性 | Greedy Search | Beam Search (k=5)
探索空間 | 1候補のみ | 5候補を保持
計算量 | O(V × T) | O(k × V × T)
メモリ | O(1) | O(k)
品質 | 局所最適 | より良い解
速度 | 最速 | 5倍遅い
推論戦略の選択基準
アプリケーション
推奨手法
理由
リアルタイム対話
Greedy Search
速度重視、低レイテンシ
機械翻訳
Beam Search (k=5-10)
品質重視、BLEU向上
文章要約
Beam Search (k=3-5)
バランス重視
創造的生成
Top-k/Nucleus Sampling
多様性重視
音声認識
Beam Search + LM
言語モデルとの統合
3.5 実践:英日機械翻訳
実装例7: 完全な翻訳パイプライン
import random
class TranslationPipeline:
"""
英日機械翻訳の完全パイプライン
"""
def __init__(self, model, src_vocab, trg_vocab, device):
self.model = model
self.src_vocab = src_vocab
self.trg_vocab = trg_vocab
self.trg_vocab_inv = {v: k for k, v in trg_vocab.items()}
self.device = device
def tokenize(self, sentence, vocab):
"""文章をトークン化"""
# 実際にはspaCyやMeCabを使用
tokens = sentence.lower().split()
indices = [vocab.get(token, vocab['
=== 英日機械翻訳パイプライン ===
--- Greedy Search翻訳 ---
EN: I love artificial intelligence
JA: 私は人工知能が好きです
EN: Machine learning is amazing
JA: 機械学習は素晴らしいです
EN: Deep neural networks are powerful
JA: ディープニューラルネットワークは強力です
--- Beam Search翻訳 (k=5) ---
EN: I love artificial intelligence
JA: 私は人工知能が大好きです
EN: Machine learning is amazing
JA: 機械学習はとても素晴らしいです
EN: Deep neural networks are powerful
JA: ディープニューラルネットワークは非常に強力です
=== 翻訳品質評価(テストセット) ===
BLEU Score:
Greedy Search: 18.5
Beam Search (k=5): 22.3
Beam Search (k=10): 23.1
訓練データ: 100,000文ペア
テストデータ: 5,000文ペア
訓練時間: 約8時間 (GPU)
推論速度: ~50文/秒 (Greedy), ~12文/秒 (Beam k=5)
Seq2Seqの課題と限界
Context Vectorのボトルネック問題
50トークン] --> B[Context Vector
512次元]
B --> C[情報損失]
C --> D[翻訳品質低下]
style B fill:#ffebee,stroke:#c62828
style C fill:#ffebee,stroke:#c62828
解決策:Attentionメカニズム
手法
Context Vector
長文性能
計算量
Vanilla Seq2Seq
最終隠れ状態のみ
低い
O(1)
Seq2Seq + Attention
全隠れ状態の重み付き和
高い
O(T × T')
Transformer
Self-Attention機構
非常に高い
O(T²)
まとめ
重要なポイント
1. Encoder-Decoderアーキテクチャ
2. Teacher Forcing
3. 推論戦略
4. 実装のポイント
requires_grad=False不要(全て学習)ignore_indexを設定(パディング対応)次のステップ
演習問題
問題1: Context Vectorの理解
問題2: Teacher Forcingの影響
問題3: Beam Searchのビーム幅選択
問題4: 系列長とメモリ使用量
問題5: Seq2Seqの応用設計