文字レベル言語モデルとデータ前処理
文字レベル言語モデルは、テキストを単語ではなく個々の文字のシーケンスとして処理します。人名生成のようなタスクでは、未知の名称や多様な綴りを柔軟に扱えるため、このアプローチが非常に有効です。
データセットの読み込みとボキャブラリ構築
モデルに入力するために、テキストデータを文字レベルのトークンに分割し、開始トークンを付与します。
def load_and_build_vocab(file_path):
with open(file_path, 'r', encoding='utf-8') as file:
raw_names = file.read().strip().split('\n')
# 先頭に開始トークンを付与
processed_names = ["^" + name for name in raw_names]
# ボキャブラリの構築
chars = sorted(list(set("".join(processed_names))))
char_to_idx = {c: i for i, c in enumerate(chars)}
idx_to_char = {i: c for c, i in char_to_idx.items()}
return processed_names, char_to_idx, idx_to_char
カスタムRNNセルの設計
PyTorchの組み込みモジュールを使用する代わりに、RNNの内部状態の更新ロジックを明示的に実装することで、シーケンス処理のメカニズムを深く理解できます。
import torch
import torch.nn as nn
import torch.nn.functional as F
class CustomRNNCell(nn.Module):
def __init__(self, vocab_size, embed_dim=32, hidden_dim=128):
super(CustomRNNCell, self).__init__()
self.hidden_dim = hidden_dim
self.char_embedding = nn.Embedding(vocab_size, embed_dim)
# 入力と前の隠れ状態を結合して次の状態を計算
self.state_transition = nn.Linear(embed_dim + hidden_dim, hidden_dim)
self.output_projection = nn.Linear(hidden_dim, vocab_size)
def init_hidden(self, batch_size):
return torch.zeros(batch_size, self.hidden_dim)
def forward(self, input_char, prev_hidden):
embedded = self.char_embedding(input_char)
combined = torch.cat((embedded, prev_hidden), dim=1)
next_hidden = torch.tanh(self.state_transition(combined))
logits = self.output_projection(next_hidden)
return next_hidden, F.log_softmax(logits, dim=-1)
訓練ループと損失関数の計算
シーケンス全体を処理し、各タイムステップでの対数確率を収集した後、交叉エントロピー損失を計算します。
def process_sequence(model, input_sequences):
seq_len, batch_sz = input_sequences.size(0), input_sequences.size(1)
hidden = model.init_hidden(batch_sz)
all_log_probs = []
for t in range(seq_len):
hidden, log_probs = model(input_sequences[t], hidden)
all_log_probs.append(log_probs)
return torch.stack(all_log_probs, dim=1)
# 損失計算のロジック
predicted_logs = sequence_log_probs[:, :-1, :]
target_chars = input_sequences[:, 1:]
# 正解ラベルの対数確率を抽出
target_logs = predicted_logs.gather(2, target_chars.unsqueeze(-1)).squeeze(-1)
sequence_loss = -target_logs.mean()
自己回帰によるテキスト生成
学習済みモデルを使用して新しいシーケンスを生成する際、Temperatureパラメータを調整することで、出力の多様性と決定論的性質のバランスを制御できます。
def generate_text(model, seed_text, max_len=50, temp=1.0):
model.eval()
char_indices = [char_to_idx[c] for c in seed_text]
input_tensor = torch.tensor([char_indices], dtype=torch.long)
hidden = model.init_hidden(1)
# シード部分の処理
for i in range(len(seed_text) - 1):
hidden, _ = model(input_tensor[:, i], hidden)
generated = list(seed_text)
current_char = input_tensor[:, -1]
with torch.no_grad():
for _ in range(max_len - len(seed_text)):
hidden, log_probs = model(current_char, hidden)
probs = torch.exp(log_probs / temp)
# torch.multinomialを使用してサンプリング
next_char_idx = torch.multinomial(probs, 1).item()
generated.append(idx_to_char[next_char_idx])
current_char = torch.tensor([[next_char_idx]], dtype=torch.long)
return "".join(generated)
Temperatureパラメータの影響:
| Temperature | 生成される名前の特徴 |
|---|---|
| 0.5 | 保守的で実在する一般的な名前に近い |
| 1.0 | 多様性と妥当性のバランスが取れている |
| 1.5 | 創造的だが、綴りが不自然な珍しい名前になる |
LSTMへの拡張とアーキテクチャの改善
基本的なRNNは長期的な依存関係の学習に苦労するため、LSTM(Long Short-Term Memory)セルに置き換えることでパフォーマンスを向上させることができます。
class CustomLSTMCell(nn.Module):
def __init__(self, vocab_size, embed_dim=32, hidden_dim=128):
super(CustomLSTMCell, self).__init__()
self.hidden_dim = hidden_dim
self.embedding_layer = nn.Embedding(vocab_size, embed_dim)
self.lstm_cell = nn.LSTMCell(embed_dim, hidden_dim)
self.fc_out = nn.Linear(hidden_dim, vocab_size)
def init_hidden(self, batch_size):
return (torch.zeros(batch_size, self.hidden_dim),
torch.zeros(batch_size, self.hidden_dim))
def forward(self, x, state):
h_prev, c_prev = state
emb = self.embedding_layer(x)
h_next, c_next = self.lstm_cell(emb, (h_prev, c_prev))
out_logits = self.fc_out(h_next)
return (h_next, c_next), F.log_softmax(out_logits, dim=-1)
訓練の安定化とデバッグテクニック
RNNの訓練では勾配爆発や消失が発生しやすいため、以下のテクニックを適用することが推奨されます。
- 勾配クリッピング:
torch.nn.utils.clip_grad_norm_を使用して勾配のノルムを制限します。 - 学習率スケジューリング: 検証セットの損失が改善しなくなった場合に学習率を減衰させます。
- 動的シーケンス長:
pack_padded_sequenceを使用して、パディングによる無駄な計算を削減します。
# 勾配クリッピングの適用例
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)
optimizer.step()