Pytorch:RNNで名前を生成する(文章生成)
はじめに
Pytorchでの
Seq2Seq
の練習として、名前生成プログラムを実装する。実装は以下のチュートリアルを参考に進めた。
Generating Names with a Character-Level RNN — PyTorch Tutorials 0.3.1.post2 documentation
目標はSeq2Seq
の理解であるが、まずは基本的なところから理解を進める。
やりたいこと
日本人の名前(カナ)を学習データとする。
先頭文字を入力すると、日本人らしい名前を生成することが目標。
学習データとしては以下のようなデータを用意した。
連番 | 性 | 名 | 姓(カタカナ) | 名(カタカナ) |
1 | 渡辺 | 郁男 | ワタナベ | イクオ |
2 | 戸塚 | 美智子 | トツカ | ミチコ |
・ | ・・ | ・・ | ・・ | ・・ |
*実際に使用するのは、5列目の「名(カタカナ)」の情報。
名前の生成に関しては下記サイトのサービスを利用した。
疑似個人情報データ生成サービス
あと、学習データとして「5000人」の名前情報を用意した。
全体イメージ
はじめ、文章生成タスクなどの場合、「正解データ(ground gruth)は何なのか」という疑問があった。
チュートリアルはこの疑問を明確に解消してくれた。
今回の場合、名前は文字ごとに区切って入力データとする。
そして、先頭文字が与えれると、そこで予測するのは「その次の文字」ということになる。
チュートリアルに載っている下図がこれを端的に表してくれている。
Generating Names with a Character-Level RNN — PyTorch Tutorials 0.3.1.post2 documentation
より引用
ネットワーク全体は下図のイメージ
前処理
まずは、CSVファイルから名前を抽出してリストに格納する。
import csv names_str = [] with open('data/names/names.csv') as f: reader = csv.reader(f) reader.next() for row in reader: names_str.append(row[4].decode('utf-8')) print(names_str[0]) # イクオ
次に、カナの辞書を作る。
all_char_str = set([char for name in names_str for char in name]) char2idx = {char: i for i, char in enumerate(all_char_str)} char2idx['EOS'] = len(char2idx) print(char2idx[u'サ']) # 13 print(len(char2idx)) # 66
LSTM
LSTMクラスの作成。
構造は標準的。
import torch import torch.nn as nn import torch.autograd as autograd from torch.autograd import Variable import torch.optim as optim class LSTM(nn.Module): def __init__(self, input_dim, embed_dim, hidden_dim): super(LSTM, self).__init__() self.hidden_dim = hidden_dim self.embeds = nn.Embedding(input_dim, embed_dim) self.lstm = nn.LSTM(embed_dim, hidden_dim) self.linear = nn.Linear(hidden_dim, input_dim) self.dropout = nn.Dropout(0.1) self.softmax = nn.LogSoftmax(dim=1) self.hidden = self.initHidden() def forward(self, input, hidden): embeds = self.embeds(input) lstm_out, hidden = self.lstm( embeds.view(len(input), 1, -1), hidden) output = self.linear(lstm_out.view(len(input), -1)) output = self.dropout(output) output = self.softmax(output) return output, hidden def initHidden(self): return (autograd.Variable(torch.zeros(1, 1, self.hidden_dim)), autograd.Variable(torch.zeros(1, 1, self.hidden_dim)))
train
訓練時の処理を関数でまとめる。
def train(model, input, target): hidden = model.initHidden() model.zero_grad() output, _ = model(input, hidden) topv, topi = output.data.topk(1) _, predY = torch.max(output.data, 1) loss = criterion(output, target) loss.backward() return loss.data[0] / input.size()[0]
あと、autograd
で文字をラップする関数を用意する。
def inputTensor(input_idx): tensor = torch.LongTensor(input_idx) return autograd.Variable(tensor) def targetTensor(input_idx): input_idx = input_idx[1:] input_idx.append(char2idx['EOS']) tensor = torch.LongTensor(input_idx) return autograd.Variable(tensor)
あとは、イテレーションさせる。
# build model model = LSTM(input_dim=len(char2idx), embed_dim=100, hidden_dim=128) criterion = nn.NLLLoss() optimizer = optim.RMSprop(model.parameters(), lr=0.001) n_iters = 4 all_losses = [] for iter in range(1, n_iters + 1): # data shuffle random.shuffle(names_idx) total_loss = 0 for i, name_idx in enumerate(names_idx): input = inputTensor(name_idx) target = targetTensor(name_idx) loss = train(model, criterion, input, target) total_loss += loss optimizer.step() print(iter, "/", n_iters) print("loss {:.4}".format(float(total_loss / len(names_idx))))
名前生成
以下のモジュールで名前を生成する。
生成方法としては、先頭文字を与えるだけ。
学習済みのネットワークが
max_length
まで文字生成を行う。ただし、出力が
<EOS>
の場合、そこで文字生成を終了する。
idx2char = {v: k for k, v in char2idx.items()} max_length = 5 def sample(start_letter='ア'): sample_char_idx = [char2idx[start_letter]] input = inputTensor(sample_char_idx) hidden = model.initHidden() output_name = start_letter for i in range(max_length): output, hidden = model(input, hidden) _, topi = output.data.topk(1) topi = topi[0][0] if topi == char2idx['EOS']: break else: letter = idx2char[topi] output_name += letter input = inputTensor([topi]) return output_name def samples(start_letters='アイウ'): for start_letter in start_letters: print(sample(start_letter)) samples(u'アスナ') # アキコ # スズネ # ナオヤ