自然言語処理における機械翻訳、テキスト要約、対話システムなど、多くのタスクでは「可変長の入力系列から可変長の出力系列への変換」が求められます。従来のRNNは固定長の入力に対して固定長の出力を返す構造であり、入力と出力の長さが異なるタスクには直接適用できませんでした。
Seq2Seq(Sequence-to-Sequence)モデル、別名Encoder-Decoderモデルは、Sutskever et al.(2014)およびCho et al.(2014)により提案された、可変長系列間の変換を可能にするアーキテクチャです。本記事では、Seq2Seqの理論的基盤を条件付き確率の定式化から丁寧に導出し、Teacher Forcingやビームサーチなどのテクニックについても解説します。最後にPythonで数列変換タスクへのSeq2Seq実装を行います。
本記事の内容
- Seq2Seqの動機と問題設定
- 条件付き確率 $P(\bm{y}|\bm{x})$ の定式化
- Encoderの構造と役割
- Decoderの構造と自己回帰生成
- Teacher Forcingによる効率的な学習
- 貪欲デコーディングとビームサーチ
- ボトルネック問題とAttention機構への動機付け
- Pythonでの数列逆順出力タスクの実装
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
Seq2Seqの問題設定
可変長入力から可変長出力への変換
入力系列 $\bm{x} = (x_1, x_2, \dots, x_{T_x})$ と出力系列 $\bm{y} = (y_1, y_2, \dots, y_{T_y})$ があり、一般に $T_x \neq T_y$ です。例として以下のようなタスクがあります。
| タスク | 入力 | 出力 | 長さの関係 |
|---|---|---|---|
| 機械翻訳 | 英語文 “I love you” | 日本語文 “私はあなたが好きです” | $T_x \neq T_y$ |
| テキスト要約 | 長い文書 | 短い要約文 | $T_x \gg T_y$ |
| 対話 | ユーザの発話 | システムの応答 | $T_x \neq T_y$ |
このような系列変換問題を、1つのモデルでend-to-endに解くのがSeq2Seqの目標です。
確率的定式化
Seq2Seqは、入力系列 $\bm{x}$ が与えられた下での出力系列 $\bm{y}$ の条件付き確率 $P(\bm{y}|\bm{x})$ を最大化するようにモデルを学習します。
確率の連鎖律(chain rule)を適用すると、
$$ P(\bm{y}|\bm{x}) = P(y_1, y_2, \dots, y_{T_y}|\bm{x}) $$
これを逐次的に分解します。
$$
\begin{align}
P(\bm{y}|\bm{x}) &= P(y_1|\bm{x}) \cdot P(y_2|y_1, \bm{x}) \cdot P(y_3|y_1, y_2, \bm{x}) \cdots P(y_{T_y}|y_1, \dots, y_{T_y-1}, \bm{x}) \\
&= \prod_{t=1}^{T_y} P(y_t | y_1, \dots, y_{t-1}, \bm{x}) \\
&= \prod_{t=1}^{T_y} P(y_t | \bm{y}_{ ここで $\bm{y}_{ この分解により、出力系列の生成を「各時刻 $t$ で、過去の出力と入力全体に基づいて次のトークンを予測する」という自己回帰的なプロセスとして定式化できます。 学習データ $\{(\bm{x}^{(n)}, \bm{y}^{(n)})\}_{n=1}^{N}$ に対して、対数尤度を最大化します。 $$
\mathcal{L} = \sum_{n=1}^{N} \log P(\bm{y}^{(n)}|\bm{x}^{(n)}) = \sum_{n=1}^{N} \sum_{t=1}^{T_y^{(n)}} \log P(y_t^{(n)} | \bm{y}_{ 実際にはこの符号を反転させた クロスエントロピー損失 を最小化します。 $$
\text{Loss} = -\frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_y^{(n)}} \log P(y_t^{(n)} | \bm{y}_{ Encoderは入力系列 $\bm{x} = (x_1, \dots, x_{T_x})$ を固定長のベクトルに圧縮する役割を担います。一般にRNN(LSTM/GRU)が用いられます。 各時刻 $t = 1, \dots, T_x$ において、Encoderは以下の再帰計算を行います。 $$
\bm{h}_t^{\text{enc}} = f_{\text{enc}}(\bm{x}_t, \bm{h}_{t-1}^{\text{enc}})
$$ ここで $f_{\text{enc}}$ はRNNセル(LSTM/GRU)の更新関数、$\bm{h}_t^{\text{enc}} \in \mathbb{R}^{h_{\text{enc}}}$ はEncoder の隠れ状態です。初期状態は $\bm{h}_0^{\text{enc}} = \bm{0}$ とします。 入力系列全体の情報は、最終時刻の隠れ状態に集約されます。 $$
\bm{c} = \bm{h}_{T_x}^{\text{enc}}
$$ このベクトル $\bm{c} \in \mathbb{R}^{h_{\text{enc}}}$ を コンテキストベクトル(context vector)と呼びます。$\bm{c}$ は入力系列 $\bm{x}$ の「要約」として機能し、Decoderに渡されます。 LSTMを用いる場合、セル状態 $\bm{c}_{T_x}^{\text{enc}}$ もDecoderの初期セル状態として渡すのが一般的です。 $$
\bm{h}_0^{\text{dec}} = \bm{h}_{T_x}^{\text{enc}}, \quad \bm{c}_0^{\text{dec}} = \bm{c}_{T_x}^{\text{enc}}
$$ EncoderとDecoderの隠れ次元が異なる場合は、線形変換を挟みます。 $$
\bm{h}_0^{\text{dec}} = \bm{W}_{\text{init}} \bm{h}_{T_x}^{\text{enc}} + \bm{b}_{\text{init}}
$$ ここで $\bm{W}_{\text{init}} \in \mathbb{R}^{h_{\text{dec}} \times h_{\text{enc}}}$ です。 Decoderは、コンテキストベクトル $\bm{c}$ を初期状態として受け取り、出力系列を1トークンずつ自己回帰的に生成します。 各時刻 $t = 1, \dots, T_y$ において、 $$
\bm{h}_t^{\text{dec}} = f_{\text{dec}}(\bm{y}_{t-1}, \bm{h}_{t-1}^{\text{dec}})
$$ ここで $\bm{y}_{t-1}$ は前の時刻に出力されたトークン(の埋め込みベクトル)、$f_{\text{dec}}$ はDecoderのRNNセルです。$t = 1$ では $\bm{y}_0$ として特別な開始トークン $\langle \text{SOS} \rangle$ を使います。 Decoderの隠れ状態 $\bm{h}_t^{\text{dec}}$ から、出力語彙 $\mathcal{V}$ 上の確率分布を計算します。 $$
P(y_t | \bm{y}_{ ここで $\bm{W}_{\text{out}} \in \mathbb{R}^{|\mathcal{V}| \times h_{\text{dec}}}$ は出力射影行列です。softmax関数は以下のように定義されます。 $$
\text{softmax}(\bm{a})_k = \frac{\exp(a_k)}{\sum_{j=1}^{|\mathcal{V}|} \exp(a_j)}
$$ 生成は特別な終了トークン $\langle \text{EOS} \rangle$ が出力されたとき、または最大出力長に達したときに終了します。 Decoderは自己回帰的に、前のステップの出力 $\hat{y}_{t-1}$ を次のステップの入力として使います。しかし、学習初期にはモデルの予測 $\hat{y}_{t-1}$ は正解とは大きくずれています。誤った予測を次の入力として使うと、誤差が累積的に増大し(exposure bias)、学習が不安定になります。 Teacher Forcingでは、訓練時にDecoderの入力として、モデルの予測 $\hat{y}_{t-1}$ ではなく正解ラベル $y_{t-1}^{*}$ を使います。 $$
\bm{h}_t^{\text{dec}} = f_{\text{dec}}(y_{t-1}^{*}, \bm{h}_{t-1}^{\text{dec}}) \quad \text{(訓練時)}
$$ これにより、各時刻のDecoderは常に正しい文脈を受け取るため、学習が安定し高速に収束します。 Teacher Forcingには exposure bias という問題があります。訓練時には常に正解が入力されますが、推論時にはモデル自身の予測(誤りを含む可能性がある)が入力されます。この訓練と推論のギャップがモデルの性能を低下させることがあります。 対策として、Scheduled Sampling(Bengio et al., 2015)があります。これは訓練中に正解ラベルを使う確率を徐々に下げ、モデル自身の予測を使う確率を上げていく手法です。 確率 $\epsilon_i$ をエポック $i$ に応じて減衰させます。 $$
\epsilon_i = \frac{k}{k + \exp(i / k)}
$$ ここで $k$ はハイパーパラメータです。確率 $\epsilon_i$ で正解ラベルを使い、$1 – \epsilon_i$ でモデルの予測を使います。 最も単純な方法は、各時刻で最も確率の高いトークンを選択することです。 $$
\hat{y}_t = \arg\max_{y \in \mathcal{V}} P(y | \hat{\bm{y}}_{ 計算量は $O(T_y \cdot |\mathcal{V}|)$ で効率的ですが、局所最適解に陥りやすいという問題があります。 ビームサーチは、上位 $B$ 個の候補(ビーム幅 $B$)を維持しながら探索を進める方法です。 アルゴリズム: スコアの長さ正規化: 短い系列のスコアが不当に有利にならないよう、長さで正規化します。 $$
\text{score}(\bm{y}) = \frac{1}{T_y^{\alpha}} \sum_{t=1}^{T_y} \log P(y_t | \bm{y}_{ $\alpha$ は通常0.6〜0.7程度に設定します。$\alpha = 0$ なら正規化なし、$\alpha = 1$ なら完全な平均です。 ビーム幅 $B = 1$ が貪欲デコーディングに一致します。$B$ を大きくすると探索空間が広がりますが、計算量は $O(T_y \cdot B \cdot |\mathcal{V}|)$ に増加します。実用上は $B = 4 \sim 10$ 程度がよく使われます。 Seq2Seqの根本的な問題は、入力系列の全情報を固定長のコンテキストベクトル $\bm{c} \in \mathbb{R}^{h}$ に圧縮しなければならない点です。 情報理論的に考えると、入力系列の長さ $T_x$ が増加すると、$\bm{c}$ に詰め込むべき情報量も増加します。しかし $\bm{c}$ の次元 $h$ は固定であるため、長い入力系列では情報の損失が避けられません。 実験的にも、Cho et al.(2014)は入力文の長さが20〜30語を超えるとBLEUスコアが急激に低下することを報告しています。 RNNの隠れ状態は逐次的に更新されるため、系列の先頭付近の情報は後方の更新によって徐々に上書きされます。たとえLSTM/GRUのゲート機構があっても、非常に長い系列の先頭情報を完全に保持することは困難です。 数学的には、$T_x$ ステップ後の隠れ状態 $\bm{h}_{T_x}$ における初期入力 $x_1$ の影響は、ゲート値の連続積によって決まります。 $$
\frac{\partial \bm{h}_{T_x}}{\partial \bm{x}_1} \propto \prod_{t=2}^{T_x} \frac{\partial \bm{h}_t}{\partial \bm{h}_{t-1}}
$$ ゲート値が1に近い次元を除けば、この勾配は指数的に減衰します。 ボトルネック問題を解決するために提案されたのがAttention機構(Bahdanau et al., 2015)です。Attentionでは、Decoderの各時刻 $t$ で、Encoderの全隠れ状態 $\bm{h}_1^{\text{enc}}, \dots, \bm{h}_{T_x}^{\text{enc}}$ に対して動的に重み(注意)を計算し、コンテキストベクトルを時刻ごとに変えます。 $$
\bm{c}_t = \sum_{s=1}^{T_x} \alpha_{t,s} \bm{h}_s^{\text{enc}}
$$ これにより、Decoderは入力系列の任意の部分に直接アクセスでき、固定長ベクトルのボトルネックを回避できます。Attention機構の詳細は後続の記事で解説します。 Seq2Seqの動作を確認するために、数列の逆順出力というシンプルなタスクを実装します。例えば入力 語彙は0〜9の数字と、特殊トークン このコードでは、GRUベースのEncoder-Decoderモデルを実装しています。Encoderが入力数列を固定長ベクトルに圧縮し、Decoderが自己回帰的に逆順の数列を生成します。Teacher Forcingを使った学習と、貪欲デコーディングによる推論を行います。 学習を進めると、短い数列(長さ3〜5)の逆順出力タスクについて、徐々に正解率が向上していく様子を確認できます。 本記事では、Seq2Seq(Encoder-Decoder)モデルについて解説しました。 次のステップとして、以下の記事も参考にしてください。学習の目的関数
Encoderの構造
入力系列の圧縮
コンテキストベクトル
Decoderの構造
自己回帰生成
出力確率の計算
生成の終了条件
Teacher Forcing
訓練時の問題
Teacher Forcingの仕組み
Teacher Forcingの問題点
デコーディング戦略
貪欲デコーディング(Greedy Decoding)
ビームサーチ(Beam Search)
貪欲法とビームサーチの比較
ボトルネック問題
固定長ベクトルの限界
RNNの忘却特性
Attention機構への動機付け
Pythonでの実装
問題設定: 数列の逆順出力
[1, 3, 5, 2] に対して [2, 5, 3, 1] を出力するタスクです。SOS(開始)、EOS(終了)、PAD(パディング)です。import numpy as np
import matplotlib.pyplot as plt
# ----------------------------
# 定数と特殊トークン
# ----------------------------
PAD = 0
SOS = 10
EOS = 11
VOCAB_SIZE = 12 # 0-9, SOS, EOS
# ----------------------------
# データ生成: 数列逆順タスク
# ----------------------------
def generate_data(n_samples, min_len=3, max_len=6):
"""ランダムな数列とその逆順をペアで生成"""
data = []
for _ in range(n_samples):
length = np.random.randint(min_len, max_len + 1)
seq = np.random.randint(1, 10, size=length).tolist() # 1-9の数列
target = seq[::-1] # 逆順
data.append((seq, target))
return data
# ----------------------------
# ユーティリティ関数
# ----------------------------
def sigmoid(x):
return 1.0 / (1.0 + np.exp(-np.clip(x, -500, 500)))
def softmax(x):
e = np.exp(x - np.max(x))
return e / np.sum(e)
def one_hot(idx, size):
"""ワンホットベクトルを生成"""
vec = np.zeros((size, 1))
vec[idx] = 1.0
return vec
# ----------------------------
# GRUセル(Encoder/Decoder共通)
# ----------------------------
class GRUCell:
def __init__(self, input_dim, hidden_dim):
self.hidden_dim = hidden_dim
scale_ih = np.sqrt(2.0 / (input_dim + hidden_dim))
scale_hh = np.sqrt(2.0 / (hidden_dim + hidden_dim))
self.Wz = np.random.randn(hidden_dim, input_dim) * scale_ih
self.Uz = np.random.randn(hidden_dim, hidden_dim) * scale_hh
self.bz = np.zeros((hidden_dim, 1))
self.Wr = np.random.randn(hidden_dim, input_dim) * scale_ih
self.Ur = np.random.randn(hidden_dim, hidden_dim) * scale_hh
self.br = np.zeros((hidden_dim, 1))
self.Wh = np.random.randn(hidden_dim, input_dim) * scale_ih
self.Uh = np.random.randn(hidden_dim, hidden_dim) * scale_hh
self.bh = np.zeros((hidden_dim, 1))
def forward(self, x, h_prev):
"""1ステップの順伝播"""
z = sigmoid(self.Wz @ x + self.Uz @ h_prev + self.bz)
r = sigmoid(self.Wr @ x + self.Ur @ h_prev + self.br)
h_tilde = np.tanh(self.Wh @ x + self.Uh @ (r * h_prev) + self.bh)
h = z * h_prev + (1 - z) * h_tilde
return h, z, r, h_tilde
def get_params(self):
return [self.Wz, self.Uz, self.bz, self.Wr, self.Ur, self.br,
self.Wh, self.Uh, self.bh]
# ----------------------------
# Seq2Seqモデル
# ----------------------------
class Seq2Seq:
def __init__(self, vocab_size, hidden_dim, lr=0.01):
self.vocab_size = vocab_size
self.hidden_dim = hidden_dim
self.lr = lr
# Encoder GRUセル
self.encoder = GRUCell(vocab_size, hidden_dim)
# Decoder GRUセル
self.decoder = GRUCell(vocab_size, hidden_dim)
# 出力層: 隠れ状態 → 語彙分布
scale = np.sqrt(2.0 / (hidden_dim + vocab_size))
self.W_out = np.random.randn(vocab_size, hidden_dim) * scale
self.b_out = np.zeros((vocab_size, 1))
def encode(self, input_seq):
"""Encoder: 入力系列 → コンテキストベクトル"""
h = np.zeros((self.hidden_dim, 1))
self.enc_states = []
for token in input_seq:
x = one_hot(token, self.vocab_size)
h, z, r, h_tilde = self.encoder.forward(x, h)
self.enc_states.append({
'x': x, 'h': h, 'z': z, 'r': r, 'h_tilde': h_tilde,
'h_prev': self.enc_states[-1]['h'] if self.enc_states else np.zeros_like(h)
})
return h # コンテキストベクトル
def decode_train(self, context, target_seq):
"""Decoder(Teacher Forcing使用): 学習時"""
h = context
self.dec_states = []
loss = 0.0
# SOS + target_seq がDecoderへの入力
input_tokens = [SOS] + target_seq
for t in range(len(target_seq)):
x = one_hot(input_tokens[t], self.vocab_size)
h_prev = h
h, z, r, h_tilde = self.decoder.forward(x, h)
# 出力確率の計算
logits = self.W_out @ h + self.b_out
probs = softmax(logits)
# クロスエントロピー損失
target_token = target_seq[t]
loss -= np.log(probs[target_token, 0] + 1e-12)
self.dec_states.append({
'x': x, 'h': h, 'h_prev': h_prev, 'z': z, 'r': r,
'h_tilde': h_tilde, 'logits': logits, 'probs': probs,
'target': target_token
})
return loss
def decode_inference(self, context, max_len=10):
"""Decoder(推論時): 貪欲デコーディング"""
h = context
output = []
token = SOS
for _ in range(max_len):
x = one_hot(token, self.vocab_size)
h, _, _, _ = self.decoder.forward(x, h)
logits = self.W_out @ h + self.b_out
probs = softmax(logits)
token = int(np.argmax(probs))
if token == EOS:
break
output.append(token)
return output
def backward(self, input_seq, target_seq):
"""逆伝播: 数値勾配で簡易実装"""
eps = 1e-5
all_params = (self.encoder.get_params()
+ self.decoder.get_params()
+ [self.W_out, self.b_out])
grads = []
for param in all_params:
grad = np.zeros_like(param)
it = np.nditer(param, flags=['multi_index'])
while not it.finished:
idx = it.multi_index
# f(x + eps)
param[idx] += eps
context_plus = self.encode(input_seq)
loss_plus = self.decode_train(context_plus, target_seq)
# f(x - eps)
param[idx] -= 2 * eps
context_minus = self.encode(input_seq)
loss_minus = self.decode_train(context_minus, target_seq)
# 中心差分
grad[idx] = (loss_plus - loss_minus) / (2 * eps)
param[idx] += eps # 元に戻す
it.iternext()
grads.append(grad)
# パラメータ更新
for param, grad in zip(all_params, grads):
np.clip(grad, -5, 5, out=grad)
param -= self.lr * grad
def train_analytic(self, input_seq, target_seq):
"""解析的な逆伝播による学習(出力層のみ高速化)"""
# 順伝播
context = self.encode(input_seq)
loss = self.decode_train(context, target_seq)
# Decoder出力層の解析的勾配
dW_out = np.zeros_like(self.W_out)
db_out = np.zeros_like(self.b_out)
# Decoder隠れ状態の勾配
dh_next = np.zeros((self.hidden_dim, 1))
# 各Decoderタイムステップを逆順に辿る
dec_dhs = []
for t in reversed(range(len(self.dec_states))):
state = self.dec_states[t]
# softmaxの勾配
dy = state['probs'].copy()
dy[state['target']] -= 1.0 # ∂L/∂logits
dW_out += dy @ state['h'].T
db_out += dy
dh = self.W_out.T @ dy + dh_next
# GRUの逆伝播
z = state['z']
r = state['r']
h_tilde = state['h_tilde']
h_prev = state['h_prev']
dz = dh * (h_prev - h_tilde)
dz_raw = dz * z * (1 - z)
dh_tilde = dh * (1 - z)
dh_tilde_raw = dh_tilde * (1 - h_tilde ** 2)
dr = (self.decoder.Uh.T @ dh_tilde_raw) * h_prev
dr_raw = dr * r * (1 - r)
# Decoder GRUのパラメータ勾配
self.decoder.Wz -= self.lr * (dz_raw @ state['x'].T)
self.decoder.Uz -= self.lr * (dz_raw @ h_prev.T)
self.decoder.bz -= self.lr * dz_raw
self.decoder.Wr -= self.lr * (dr_raw @ state['x'].T)
self.decoder.Ur -= self.lr * (dr_raw @ h_prev.T)
self.decoder.br -= self.lr * dr_raw
self.decoder.Wh -= self.lr * (dh_tilde_raw @ state['x'].T)
self.decoder.Uh -= self.lr * (dh_tilde_raw @ (r * h_prev).T)
self.decoder.bh -= self.lr * dh_tilde_raw
# 前時刻への勾配伝播
dh_next = (dh * z
+ self.decoder.Uz.T @ dz_raw
+ self.decoder.Ur.T @ dr_raw
+ (self.decoder.Uh.T @ dh_tilde_raw) * r)
dec_dhs.append(dh_next)
# 出力層パラメータ更新
np.clip(dW_out, -5, 5, out=dW_out)
np.clip(db_out, -5, 5, out=db_out)
self.W_out -= self.lr * dW_out
self.b_out -= self.lr * db_out
# Encoderの逆伝播(dh_nextをEncoderに伝播)
dh_enc = dh_next
for t in reversed(range(len(self.enc_states))):
state = self.enc_states[t]
z = state['z']
r = state['r']
h_tilde = state['h_tilde']
h_prev = state['h_prev']
dz = dh_enc * (h_prev - h_tilde)
dz_raw = dz * z * (1 - z)
dh_tilde = dh_enc * (1 - z)
dh_tilde_raw = dh_tilde * (1 - h_tilde ** 2)
dr = (self.encoder.Uh.T @ dh_tilde_raw) * h_prev
dr_raw = dr * r * (1 - r)
self.encoder.Wz -= self.lr * (dz_raw @ state['x'].T)
self.encoder.Uz -= self.lr * (dz_raw @ h_prev.T)
self.encoder.bz -= self.lr * dz_raw
self.encoder.Wr -= self.lr * (dr_raw @ state['x'].T)
self.encoder.Ur -= self.lr * (dr_raw @ h_prev.T)
self.encoder.br -= self.lr * dr_raw
self.encoder.Wh -= self.lr * (dh_tilde_raw @ state['x'].T)
self.encoder.Uh -= self.lr * (dh_tilde_raw @ (r * h_prev).T)
self.encoder.bh -= self.lr * dh_tilde_raw
dh_enc = (dh_enc * z
+ self.encoder.Uz.T @ dz_raw
+ self.encoder.Ur.T @ dr_raw
+ (self.encoder.Uh.T @ dh_tilde_raw) * r)
return loss
# ----------------------------
# 学習の実行
# ----------------------------
np.random.seed(42)
# ハイパーパラメータ
hidden_dim = 32
lr = 0.005
n_epochs = 100
n_train = 500
n_test = 100
# データ生成
train_data = generate_data(n_train, min_len=3, max_len=5)
test_data = generate_data(n_test, min_len=3, max_len=5)
# モデル初期化
model = Seq2Seq(VOCAB_SIZE, hidden_dim, lr=lr)
# 学習ループ
losses = []
accuracies = []
for epoch in range(n_epochs):
epoch_loss = 0.0
np.random.shuffle(train_data)
for input_seq, target_seq in train_data:
target_with_eos = target_seq + [EOS]
loss = model.train_analytic(input_seq, target_with_eos)
epoch_loss += loss
avg_loss = epoch_loss / n_train
losses.append(avg_loss)
# テスト精度の計算
if (epoch + 1) % 10 == 0:
correct = 0
for input_seq, target_seq in test_data:
context = model.encode(input_seq)
pred = model.decode_inference(context, max_len=len(target_seq) + 2)
if pred == target_seq:
correct += 1
acc = correct / n_test
accuracies.append((epoch + 1, acc))
print(f"Epoch {epoch+1}/{n_epochs} Loss: {avg_loss:.4f} "
f"Test Accuracy: {acc:.2%}")
# ----------------------------
# 学習曲線の可視化
# ----------------------------
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
# 損失曲線
ax1.plot(range(1, n_epochs + 1), losses, linewidth=2)
ax1.set_xlabel("Epoch")
ax1.set_ylabel("Cross-Entropy Loss")
ax1.set_title("Training Loss")
ax1.grid(True, alpha=0.3)
# 精度曲線
if accuracies:
epochs_acc, accs = zip(*accuracies)
ax2.plot(epochs_acc, accs, 'o-', linewidth=2, markersize=8)
ax2.set_xlabel("Epoch")
ax2.set_ylabel("Sequence Accuracy")
ax2.set_title("Test Accuracy (Exact Match)")
ax2.set_ylim([0, 1.05])
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# ----------------------------
# 推論例の表示
# ----------------------------
print("\n--- 推論例 ---")
for i in range(10):
input_seq, target_seq = test_data[i]
context = model.encode(input_seq)
pred = model.decode_inference(context, max_len=len(target_seq) + 2)
status = "OK" if pred == target_seq else "NG"
print(f"入力: {input_seq} 正解: {target_seq} 予測: {pred} [{status}]")
まとめ