機械学習・自然言語処理の勉強メモ

学んだことのメモやまとめ

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)は何なのか」という疑問があった。
チュートリアルはこの疑問を明確に解消してくれた。
今回の場合、名前は文字ごとに区切って入力データとする。
そして、先頭文字が与えれると、そこで予測するのは「その次の文字」ということになる。

チュートリアルに載っている下図がこれを端的に表してくれている。
f:id:kento1109:20180411114320p:plain
Generating Names with a Character-Level RNN — PyTorch Tutorials 0.3.1.post2 documentation
より引用

ネットワーク全体は下図のイメージ

f:id:kento1109:20180411115917p:plain

前処理



まずは、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'アスナ')
# アキコ
# スズネ
# ナオヤ