🔄 1. Seq2Seqモデルとは?
Seq2Seq(Sequence-to-Sequence)モデルは、
可変長の入力系列を可変長の出力系列に変換するモデルです。
2014年にGoogleが発表し、機械翻訳の精度を大幅に向上させました。
1-1. 従来のモデルの問題
【従来のLSTMの制約】
■ テキスト分類(STEP 8-9で学んだ内容)
入力: “This movie is great” (4単語)
出力: positive/negative (1つの値)
→ 入力は可変長OK
→ 出力は固定長(クラス数)
■ 問題となるタスク
機械翻訳:
入力: “I love you” (3単語)
出力: “私はあなたを愛しています” (7単語?)
→ 入力と出力の長さが違う!
→ 出力の長さが事前にわからない!
→ 従来のLSTMでは対応困難
1-2. Seq2Seqの解決策
💡 Seq2Seqの核心アイデア
「2つのLSTMを組み合わせる」
- Encoder(エンコーダ):入力を理解して「意味」に変換
- Decoder(デコーダ):「意味」から出力を生成
【Seq2Seqのイメージ】
入力: “I love you”(英語)
│
↓
┌─────────────────┐
│ Encoder │ ← 英語を理解
│ (LSTM) │
└────────┬────────┘
│
「愛の意味」(ベクトル)
│
↓
┌─────────────────┐
│ Decoder │ ← 日本語で表現
│ (LSTM) │
└────────┬────────┘
│
↓
出力: “私はあなたを愛しています”(日本語)
ポイント:
・入力の長さ(3単語)と出力の長さ(7単語)が異なる
・中間に「意味」を表すベクトルがある
1-3. Seq2Seqの応用タスク
| タスク |
入力例 |
出力例 |
| 機械翻訳 |
“Good morning” |
“おはようございます” |
| 文章要約 |
長い記事(200語) |
要約(30語) |
| チャットボット |
“How are you?” |
“I’m fine!” |
| 質問応答 |
“日本の首都は?” |
“東京です” |
🏗️ 2. Encoder-Decoderアーキテクチャ
Seq2Seqモデルの中核はEncoder(エンコーダ)と
Decoder(デコーダ)の2つの部分です。
2-1. 全体構造
【Encoder-Decoderの全体像】
入力: “I love you”
┌─────────────────────────────────────────────────┐
│ Seq2Seqモデル │
│ │
│ Encoder Decoder │
│ ┌───────┐ ┌───────┐ │
│ │ │ Context │ │ │
│ │ LSTM │────Vector───────→│ LSTM │ │
│ │ │ (意味) │ │ │
│ └───┬───┘ └───┬───┘ │
│ ↑ ↓ │
│ 入力系列 出力系列 │
│ “I love you” “私は…愛しています” │
└─────────────────────────────────────────────────┘
処理の流れ:
1. Encoder: 入力系列を読み込み、最終状態を出力
2. Context Vector: Encoderの最終状態(入力の「意味」)
3. Decoder: Context Vectorを初期状態として、1単語ずつ生成
2-2. Encoder(エンコーダ)の仕組み
📥 Encoderの役割
「入力系列の意味を1つのベクトルに圧縮する」
【Encoderの処理(例: “I love you”)】
入力: [“I”, “love”, “you”]
■ ステップ1: “I”を処理
入力: “I”のベクトル [0.1, 0.2, -0.3, …]
前の隠れ状態: h0 = ゼロベクトル(初期状態)
↓
LSTM セル
↓
出力: h1 = [-0.2, 0.5, 0.1, …]
(”I”の情報を含む)
■ ステップ2: “love”を処理
入力: “love”のベクトル [0.8, -0.1, 0.5, …]
前の隠れ状態: h1
↓
LSTM セル
↓
出力: h2 = [0.3, 0.7, -0.2, …]
(”I love”の情報を含む)
■ ステップ3: “you”を処理
入力: “you”のベクトル [-0.1, 0.3, 0.2, …]
前の隠れ状態: h2
↓
LSTM セル
↓
出力: h3 = [0.5, -0.3, 0.8, …]
(”I love you”の情報を含む)
★ Context Vector = h3
→ 文全体の意味を表すベクトル
2-3. Context Vector(コンテキストベクトル)
【Context Vectorとは?】
“I love you” → Encoder → Context Vector
Context Vector = [0.5, -0.3, 0.8, 0.2, -0.1, …]
(hidden_dim次元のベクトル、例: 512次元)
このベクトルは何を表す?
・「愛の告白」という意味
・主語が「私」
・対象が「あなた」
・感情が「ポジティブ」
…などの情報が詰まっている
比喩:
・英語の文を読んで、頭の中で「意味」を理解
・その「理解」がContext Vector
・日本語に翻訳するとき、この「理解」を元に言葉を選ぶ
⚠️ Context Vectorの問題点
情報のボトルネック:
- どんなに長い文も、1つのベクトルに圧縮される
- 長い文では、最初の方の情報が失われる
- 例: 100単語の文 → 512次元のベクトル(情報が失われる)
解決策:Attention(次のSTEP 11で学習)
2-4. Decoder(デコーダ)の仕組み
📤 Decoderの役割
「Context Vectorから出力系列を1単語ずつ生成する」
【Decoderの処理(例: “私はあなたを愛しています”を生成)】
初期状態 = Context Vector(Encoderの最終状態)
■ ステップ1: 最初の単語を生成
入力: <START>トークン(開始を示す特別なトークン)
隠れ状態: Context Vector
↓
LSTM セル
↓
出力: 各単語の確率
“私”: 0.8 ← 最大
“あなた”: 0.1
“愛”: 0.05
…
→ “私”を選択
■ ステップ2: 次の単語を生成
入力: “私”(前のステップで生成した単語)
隠れ状態: h1(ステップ1の出力)
↓
LSTM セル
↓
出力: 各単語の確率
“は”: 0.7 ← 最大
“が”: 0.2
…
→ “は”を選択
■ ステップ3〜7: 同様に続く
“あなた” → “を” → “愛し” → “て” → “います”
■ ステップ8: 終了
入力: “います”
出力: <END>トークン(終了を示す特別なトークン)
→ 生成終了
最終出力: “私はあなたを愛しています”
2-5. 特別なトークン
【Seq2Seqで使う特別なトークン】
1. <START> または <BOS> (Beginning of Sentence)
– Decoderの最初の入力
– 「ここから文を始める」という合図
2. <END> または <EOS> (End of Sentence)
– 生成の終了を示す
– このトークンが出力されたら生成を停止
3. <PAD> (Padding)
– バッチ処理時に長さを揃えるための詰め物
4. <UNK> (Unknown)
– 語彙にない未知語
例:
入力: “I love you”
→ [<START>, I, love, you, <END>, <PAD>, <PAD>]
出力: “私は愛しています”
→ [<START>, 私, は, 愛し, てい, ます, <END>]
👨🏫 3. Teacher Forcing(教師強制)
Seq2Seqモデルの訓練にはTeacher Forcingという特別な手法を使います。
3-1. 問題:訓練時の困難
【訓練時の問題】
正解: “私はあなたを愛しています”
■ 普通に訓練する場合(自己回帰)
ステップ1: <START> → 予測: “私” ✅
ステップ2: “私” → 予測: “が” ❌(正解は”は”)
ステップ3: “が” → 予測: “あなた”
↑
間違った入力から予測することになる!
問題:
・ステップ2で間違えると、ステップ3以降も影響を受ける
・誤りが連鎖して、正しい出力を学習できない
・学習が不安定になる
3-2. Teacher Forcingの解決策
💡 Teacher Forcingのアイデア
「訓練時は、予測結果ではなく正解を次の入力にする」
【Teacher Forcingの動作】
正解: “私はあなたを愛しています”
■ Teacher Forcingあり(訓練時)
ステップ1: <START> → 予測: “私” ✅
ステップ2: “私”(正解)→ 予測: “が” ❌
↑
予測が間違っていても、正解の”私”を入力として使う
ステップ3: “は”(正解)→ 予測: “あなた”
↑
予測が間違っていても、正解の”は”を入力として使う
…
効果:
・各ステップが独立に学習できる
・誤りが連鎖しない
・学習が安定して速い
3-3. 訓練と推論の違い
【訓練時 vs 推論時】
■ 訓練時(Teacher Forcing)
入力: 正解の単語
目的: 正しい単語を予測する確率を上げる
<START> → “私”を予測
“私”(正解)→ “は”を予測
“は”(正解)→ “あなた”を予測
…
■ 推論時(実際の使用時)
入力: 自分の予測結果
目的: 文を生成する
<START> → “私”を予測
“私”(予測)→ “は”を予測
“は”(予測)→ “あなた”を予測
…
問題: 訓練と推論で動作が違う!
→ Exposure Bias問題
3-4. Exposure Bias問題と対策
⚠️ Exposure Bias問題
問題:
- 訓練時: 常に正解を入力として使う
- 推論時: 自分の予測(誤りの可能性あり)を使う
- 訓練時に「誤った入力」を見たことがない
- 推論時に誤りがあると、そこから回復できない
【対策: Scheduled Sampling】
アイデア: 訓練中にTeacher Forcingの確率を徐々に下げる
■ 訓練の初期(エポック1-5)
Teacher Forcing確率: 100%
→ まずは正解を見ながら基本を学ぶ
■ 訓練の中期(エポック6-10)
Teacher Forcing確率: 50%
→ 半分は正解、半分は自分の予測を使う
■ 訓練の後期(エポック11-15)
Teacher Forcing確率: 20%
→ ほぼ推論時と同じ状況で訓練
効果:
・訓練と推論のギャップを縮める
・誤りから回復する能力が育つ
🔍 4. 推論時の生成手法
訓練が終わったら、実際に文を生成(推論)します。
生成方法にはいくつかの選択肢があります。
4-1. Greedy Search(貪欲法)
💡 Greedy Searchのアイデア
「各ステップで最も確率の高い単語を選ぶ」
【Greedy Searchの例】
入力: “I love you” → 翻訳
■ ステップ1: <START>
各単語の予測確率:
“私”: 0.60 ← 最大!
“僕”: 0.25
“俺”: 0.10
その他: 0.05
→ “私”を選択
■ ステップ2: “私”
各単語の予測確率:
“は”: 0.70 ← 最大!
“が”: 0.20
“も”: 0.05
その他: 0.05
→ “は”を選択
■ ステップ3: “は”
各単語の予測確率:
“あなた”: 0.65 ← 最大!
“君”: 0.20
“彼”: 0.10
その他: 0.05
→ “あなた”を選択
… 続く …
最終出力: “私はあなたを愛しています”
特徴:
✅ シンプルで実装が簡単
✅ 計算が速い
❌ 局所最適解に陥る可能性
❌ 最初の選択が間違うと全体が崩れる
4-2. Beam Search(ビームサーチ)
🔦 Beam Searchのアイデア
「複数の候補を保持しながら探索する」
【Beam Search(beam_size=3)の例】
入力: “I love you” → 翻訳
■ ステップ1: <START>
上位3つを保持:
候補1: “私” (確率: 0.60)
候補2: “僕” (確率: 0.25)
候補3: “俺” (確率: 0.10)
■ ステップ2: 各候補から展開
候補1 “私” から:
“私 は” (0.60 × 0.70 = 0.42)
“私 が” (0.60 × 0.20 = 0.12)
“私 も” (0.60 × 0.05 = 0.03)
候補2 “僕” から:
“僕 は” (0.25 × 0.50 = 0.125)
“僕 が” (0.25 × 0.30 = 0.075)
“僕 も” (0.25 × 0.10 = 0.025)
候補3 “俺” から:
“俺 は” (0.10 × 0.40 = 0.04)
…
上位3つを選択:
候補1: “私 は” (0.420)
候補2: “僕 は” (0.125)
候補3: “私 が” (0.120)
■ ステップ3: 同様に続く
…
最終的に最も確率が高い系列を選択
特徴:
✅ より良い解を見つけやすい
✅ 局所最適解を避けられる
❌ 計算量が増える(beam_size倍)
❌ 多様性が低い(似た候補が残りやすい)
4-3. Greedy vs Beam の比較
| 項目 |
Greedy Search |
Beam Search |
| 保持する候補数 |
1つ |
k個(beam_size) |
| 計算速度 |
速い |
k倍遅い |
| 精度 |
やや低い |
高い |
| 推奨用途 |
高速化が必要な場合 |
精度が重要な場合 |
| 推奨値 |
– |
beam_size = 5〜10 |
💻 5. PyTorchでのSeq2Seq実装
Seq2SeqモデルをPyTorchで実装します。
Encoder、Decoder、Seq2Seq全体を段階的に作成します。
5-1. Encoderの実装
ステップ1:クラスの定義と初期化
import torch
import torch.nn as nn
class Encoder(nn.Module):
“””Seq2SeqのEncoder(LSTM)”””
def __init__(self, input_dim, embedding_dim, hidden_dim, n_layers=1, dropout=0.5):
super(Encoder, self).__init__()
# パラメータの意味:
# input_dim: 入力語彙サイズ(例: 英語の単語数)
# embedding_dim: 埋め込み次元(例: 256)
# hidden_dim: LSTMの隠れ状態の次元(例: 512)
# n_layers: LSTMの層数
# dropout: ドロップアウト率
self.hidden_dim = hidden_dim
self.n_layers = n_layers
# Embedding層: 単語ID → ベクトル
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 # (batch, seq, feature)の順
)
self.dropout = nn.Dropout(dropout)
ステップ2:forwardメソッド
def forward(self, src):
“””
Args:
src: 入力系列 (batch_size, src_length)
例: [[2, 5, 8, 3], [4, 6, 9, 1]] (バッチサイズ2、系列長4)
Returns:
hidden: 最終隠れ状態 (n_layers, batch_size, hidden_dim)
cell: 最終セル状態 (n_layers, batch_size, hidden_dim)
“””
# 1. 埋め込み: 単語ID → ベクトル
embedded = self.dropout(self.embedding(src))
# embedded: (batch_size, src_length, embedding_dim)
# 2. LSTM処理
# outputs: 全時刻の出力(今回は使わない)
# hidden: 最終時刻の隠れ状態 → Context Vector
# cell: 最終時刻のセル状態
outputs, (hidden, cell) = self.lstm(embedded)
# Context Vector = hidden(Encoderの最終状態)
return hidden, cell
5-2. Decoderの実装
ステップ1:クラスの定義と初期化
class Decoder(nn.Module):
“””Seq2SeqのDecoder(LSTM)”””
def __init__(self, output_dim, embedding_dim, hidden_dim, n_layers=1, dropout=0.5):
super(Decoder, self).__init__()
# パラメータの意味:
# output_dim: 出力語彙サイズ(例: 日本語の単語数)
# その他はEncoderと同じ
self.output_dim = output_dim
self.hidden_dim = hidden_dim
self.n_layers = n_layers
# Embedding層
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)
ステップ2:forwardメソッド
def forward(self, input, hidden, cell):
“””
Decoderは1単語ずつ生成する
Args:
input: 現在の入力単語 (batch_size, 1)
hidden: 前の隠れ状態 (n_layers, batch_size, hidden_dim)
cell: 前のセル状態 (n_layers, batch_size, hidden_dim)
Returns:
prediction: 次の単語の確率 (batch_size, output_dim)
hidden: 更新された隠れ状態
cell: 更新されたセル状態
“””
# 1. 埋め込み
embedded = self.dropout(self.embedding(input))
# embedded: (batch_size, 1, embedding_dim)
# 2. LSTM(1ステップだけ)
output, (hidden, cell) = self.lstm(embedded, (hidden, cell))
# output: (batch_size, 1, hidden_dim)
# 3. 出力層で単語の確率を計算
# squeeze(1): 系列長の次元を削除 (batch_size, 1, hidden_dim) → (batch_size, hidden_dim)
prediction = self.fc_out(output.squeeze(1))
# prediction: (batch_size, output_dim)
return prediction, hidden, cell
5-3. Seq2Seq全体の実装
import random
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_length)
trg: 正解系列 (batch_size, trg_length)
teacher_forcing_ratio: Teacher Forcingを使う確率
Returns:
outputs: 全時刻の予測 (batch_size, trg_length, output_dim)
“””
batch_size = src.shape[0]
trg_length = trg.shape[1]
trg_vocab_size = self.decoder.output_dim
# 出力を格納するテンソル
outputs = torch.zeros(batch_size, trg_length, trg_vocab_size).to(self.device)
# 1. Encoderで入力を処理
hidden, cell = self.encoder(src)
# hidden, cell: Context Vector(入力の「意味」)
# 2. Decoderの最初の入力は<START>トークン
# trg[:, 0]: 正解系列の最初の要素(= <START>)
input = trg[:, 0].unsqueeze(1) # (batch_size, 1)
# 3. 1単語ずつ生成
for t in range(1, trg_length):
# Decoderで1単語予測
output, hidden, cell = self.decoder(input, hidden, cell)
# output: (batch_size, output_dim)
# 予測を保存
outputs[:, t] = output
# Teacher Forcingを使うか決定
teacher_force = random.random() < teacher_forcing_ratio
# 最も確率の高い単語を取得
top1 = output.argmax(1).unsqueeze(1) # (batch_size, 1)
# 次の入力を決定
# Teacher Forcing: 正解を使う
# そうでなければ: 自分の予測を使う
input = trg[:, t].unsqueeze(1) if teacher_force else top1
return outputs
5-4. モデルの作成と確認
# デバイスの設定
device = torch.device(‘cuda’ if torch.cuda.is_available() else ‘cpu’)
# パラメータ
INPUT_DIM = 10000 # 入力語彙サイズ(英語)
OUTPUT_DIM = 10000 # 出力語彙サイズ(日本語)
ENC_EMB_DIM = 256 # Encoder埋め込み次元
DEC_EMB_DIM = 256 # Decoder埋め込み次元
HID_DIM = 512 # 隠れ状態の次元
N_LAYERS = 2 # LSTM層数
DROPOUT = 0.5 # ドロップアウト率
# Encoder, Decoder, Seq2Seqの作成
encoder = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, DROPOUT)
decoder = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DROPOUT)
model = Seq2Seq(encoder, decoder, device).to(device)
print(model)
print(f”\nパラメータ数: {sum(p.numel() for p in model.parameters()):,}”)
実行結果:
Seq2Seq(
(encoder): Encoder(
(embedding): Embedding(10000, 256)
(lstm): LSTM(256, 512, num_layers=2, batch_first=True, dropout=0.5)
(dropout): Dropout(p=0.5, inplace=False)
)
(decoder): Decoder(
(embedding): Embedding(10000, 256)
(lstm): LSTM(256, 512, num_layers=2, batch_first=True, dropout=0.5)
(fc_out): Linear(in_features=512, out_features=10000, bias=True)
(dropout): Dropout(p=0.5, inplace=False)
)
)
パラメータ数: 25,684,000
🎯 6. 訓練と推論
6-1. 訓練関数
import torch.optim as optim
# 損失関数(パディングを無視)
PAD_IDX = 0 # <PAD>のインデックス
criterion = nn.CrossEntropyLoss(ignore_index=PAD_IDX)
# 最適化器
optimizer = optim.Adam(model.parameters(), lr=0.001)
def train_epoch(model, dataloader, optimizer, criterion, clip=1.0):
“””1エポックの訓練”””
model.train()
total_loss = 0
for src, trg in dataloader:
src, trg = src.to(device), trg.to(device)
# 勾配をゼロに
optimizer.zero_grad()
# Forward pass(Teacher Forcing率50%)
output = model(src, trg, teacher_forcing_ratio=0.5)
# output: (batch_size, trg_length, output_dim)
# 損失計算
# <START>を除く(最初のトークンは予測しない)
output_dim = output.shape[-1]
output = output[:, 1:].reshape(-1, output_dim) # (batch_size * (trg_len-1), output_dim)
trg = trg[:, 1:].reshape(-1) # (batch_size * (trg_len-1))
loss = criterion(output, trg)
# Backward pass
loss.backward()
# 勾配クリッピング(重要!)
torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
# パラメータ更新
optimizer.step()
total_loss += loss.item()
return total_loss / len(dataloader)
6-2. 推論関数(翻訳)
def translate(model, src_sentence, src_vocab, trg_vocab, max_length=50):
“””
文を翻訳する(Greedy Search)
Args:
model: 訓練済みSeq2Seqモデル
src_sentence: 入力文(単語のリスト)
src_vocab: 入力語彙(単語→ID)
trg_vocab: 出力語彙(ID→単語)
max_length: 最大生成長
Returns:
翻訳結果(単語のリスト)
“””
model.eval()
# 1. 入力をIDに変換
src_ids = [src_vocab.get(word, src_vocab[‘<unk>’]) for word in src_sentence]
src_tensor = torch.LongTensor(src_ids).unsqueeze(0).to(device)
# src_tensor: (1, src_length)
with torch.no_grad():
# 2. Encoderで入力を処理
hidden, cell = model.encoder(src_tensor)
# 3. Decoderで1単語ずつ生成
trg_ids = [trg_vocab[‘<start>’]]
for _ in range(max_length):
# 現在の入力
input = torch.LongTensor([trg_ids[-1]]).unsqueeze(0).to(device)
# Decoder
output, hidden, cell = model.decoder(input, hidden, cell)
# 最も確率が高い単語
pred_id = output.argmax(1).item()
trg_ids.append(pred_id)
# <END>が出たら終了
if pred_id == trg_vocab[‘<end>’]:
break
# 4. IDを単語に変換
id_to_word = {v: k for k, v in trg_vocab.items()}
translation = [id_to_word[id] for id in trg_ids[1:-1]] # <START>と<END>を除く
return translation
⚠️ 7. Seq2Seqの課題と限界
Seq2Seqは画期的なモデルでしたが、いくつかの重要な課題があります。
7-1. 情報のボトルネック
🔴 問題:Context Vectorに全てを詰め込む
入力文全体の意味を1つの固定長ベクトルに圧縮するため、
長い文では情報が失われます。
【情報のボトルネックの例】
■ 短い文(問題なし)
入力: “I love you”(3単語)
Context Vector: 512次元
→ 十分に表現できる
■ 長い文(問題あり)
入力: “The cat, which was sitting on the mat near the
window where the sunlight was streaming in,
suddenly jumped up and ran away.”(30単語)
Context Vector: 512次元(同じ!)
→ 情報が失われる
特に:
・文の最初の方の情報が薄れる
・細かいニュアンスが失われる
・固有名詞などが正しく翻訳されない
7-2. 長期依存関係の問題
【長期依存関係の例】
入力: “The keys, which were on the table next to the door
where I left them yesterday morning, are missing.”
問題:
・”keys”と”are”が遠く離れている
・その間に多くの修飾語がある
・LSTMでも情報が薄れてしまう
結果:
・主語と動詞の一致が崩れる
・翻訳の品質が低下する
7-3. Seq2Seqの位置づけ
💡 Seq2Seqの歴史的な意義
2014年:Seq2Seq登場
- 機械翻訳の精度が大幅に向上
- End-to-End学習の可能性を示した
- NLPに革命をもたらした
現在の位置づけ:
- 基礎概念として重要(学習目的)
- 実務ではTransformerが主流
- 次のAttentionを理解するための前提知識
📊 Seq2Seqからの進化
2014年:Seq2Seq → 情報のボトルネック問題
2015年:Attention追加 → 問題を大幅に改善
2017年:Transformer → RNNを完全に置き換え
2018年〜:BERT, GPT → 現代のNLPの標準