どん底から這い上がるまでの記録

どん底から這い上がりたいけど這い上がれない人がいろいろ書くブログ(主にプログラミング)

RNNを使った文章の自動生成

スポンサーリンク


今回はRNNを使った文章の自動生成をやってみます。

今回やりたいことは単語を学習したモデルに渡して、その単語から次の単語を予測。これを繰り返して文章を生成することです。

実装はPyTorchです。

RNNでの学習にはある程度の長い文章が必要になってくるので、Wikipediaの文章を使ってみます。ただ、自分の環境ではあまりに長い文章の学習はできないので、3つの記事に絞ってやってみます。

 

環境

  • Windows10
  • PyTorch 0.2.1
  • Python 3.6.2

1. Wikipediaからデータを取ってくる

Wikipediaから学習データを取ってきます。今回は「織田信長」、「豊臣秀吉」、「徳川家康」の3つの記事を学習データとして使います。

次のプログラムでWikipediaからデータを取ってきて、少し前処理をしてファイルに保存しています。

get_dataset.py

# -*- coding: utf-8 -*-
import MeCab
import codecs
import re
import urllib.parse as par
import urllib.request as req


def write2file(fname, sentences):
    with codecs.open(fname, 'w', 'utf-8') as f:
        f.write("".join(sentences))

def get_morphemes(sentences):
    morphemes = []
    for sent in sentences:
        if len(sent) == 0:
            continue
        temp = tagger.parse(sent).split()
        temp.append("。\n")
        morphemes.append(" ".join(temp))
    return morphemes if morphemes else -1


tagger = MeCab.Tagger("-Owakati")
link = "https://ja.wikipedia.org/wiki/"
fname_list = ["nobunaga", "hideyoshi", "ieyasu"]
word_list = ["織田信長", "豊臣秀吉", "徳川家康"]
for fname, word in zip(fname_list, word_list):
    with req.urlopen(link + par.quote_plus(word)) as response:
        html = response.read().decode('utf-8')
        # <p>タグを取得
        all_p_tag = re.findall("<p>.*</p>", html)
        temp = []
        for p in all_p_tag:
            # 半角文字を削除
            p = re.sub("[\s!-~]*", "", p)
            p = p.split("。")
            # 分かち書き
            morphemes = get_morphemes(p)
            if morphemes == -1:
                continue
            temp = temp + morphemes
        write2file(fname + ".txt", temp)

上のプログラムでやってることは、まずWikipediaからデータを取ってきます。

<p>タグで囲まれた文章のみを取り出し半角文字を取り除く。

前処理したデータを形態素解析分かち書きした後ファイルに書き込んでいます。

 

2. 単語辞書を作り、学習データをインデックスに変換する

data_util.py

# -*- coding: utf-8 -*-
import codecs


def read_file(fname):
    """ Read file
    :param fname: file name
    :return: word list in the file
    """
    with codecs.open(fname + ".txt", 'r', 'utf-8') as f:
        return f.read().splitlines()

def select_sentences(sentences):
    dataset = []
    for sent in sentences:
        morphemes = sent.split()
        if len(morphemes) > 30:
            continue
        for i in range(len(morphemes)-2):
            if morphemes[i] == morphemes[i+1]:
                break
            if morphemes[i] == morphemes[i+2]:
                break
        else:
            dataset.append(sent)
    return dataset

def make_vocab(sentences):
    """ make dictionary
    :param sentences: word list ex. ["I", "am", "stupid"]
    """
    global word2id

    for sent in sentences:
        for morpheme in sent.split():
            if morpheme in word2id:
                continue
            word2id[morpheme] = len(word2id)

def sent2id(sentences):
    id_list = []
    for sent in sentences:
        temp = []
        for morpheme in sent.split():
            temp.append(word2id[morpheme])
        id_list.append(temp)
    return id_list

def get_dataset():
    fname_list = ["nobunaga", "hideyoshi", "ieyasu"]
    dataset = []
    # make dictionary
    for fname in fname_list:
        sentences = read_file(fname)
        sentences = select_sentences(sentences)
        make_vocab(sentences)
        dataset = dataset + sentences
    id2sent = sent2id(dataset)
    return word2id, id2sent, dataset


word2id = {}

上のプログラムをdata_util.pyとします。

このプログラムでやりたいことは、1.で生成したファイルを読み込んで、学習データとして使えないものを排除します。

なぜなら、1で少し前処理をしましたがこれでは不十分だからです。1で生成したファイルの中身を見ると以下のようになっています。

織田 信長 ( おだ のぶ な が ) は 、 戦国 時代 日本 戦国 時代 から 安土 桃山 時代 安土 桃山 時代 にかけて の 武将 武将 ・ 戦国 大名 戦国 大名 。

三 英傑 三 英傑 の 一 人 。

尾張尾張 国 ( 現在 の 愛知 県 愛知 県 ) の 古渡 城 古渡 城主 ・ 織田 信秀 織田 信秀 の 嫡男 嫡男 注釈 。

少し前処理しただけでは同じ単語が連続して続いていたりするので、学習データを作る際にselect_sentencesメソッドで文章が上のようなものになっているかを判別して取り除くようにします。

さらに、学習に使う文章に含まれる形態素が30以下のものを学習データとして使います。

そして、出来上がったものから単語辞書を作り、単語辞書を元にして学習データを文字列から整数へと変換します。

 

3. ネットワークの定義

ネットワークは下のような感じで作りました。

3層からなるRecurrent Neural Networkです。

USE_CUDA = False
FloatTensor = torch.cuda.FloatTensor if USE_CUDA else torch.FloatTensor
LongTensor = torch.cuda.LongTensor if USE_CUDA else torch.LongTensor
class RNN(nn.Module):
    def __init__(self, n_vocab, embedding_dim, hidden_dim):
        super(RNN, self).__init__()
        self.encoder = nn.Embedding(n_vocab, embedding_dim)
        self.gru = nn.GRU(embedding_dim, hidden_dim)
        self.decoder = nn.Linear(hidden_dim, n_vocab)
        self.embedding_dim = embedding_dim
        self.num_layers = 1
        self.init_hidden()

    def init_hidden(self):
        self.hidden = Variable(\
                        FloatTensor(self.num_layers, 1, self.embedding_dim).fill_(0))
        if USE_CUDA:
            self.hidden.cuda()

    def forward(self, x):
        x = self.encoder(x.view(1, -1))
        y, self.hidden = self.gru(x.view(1, 1, -1), self.hidden)
        y = self.decoder(y.view(1, -1))
        return y

 

4. 学習する

文章をモデルに渡して学習させます。

例えば、下のような文章があったとき、

幼名 は 吉 法師 ( き っぽ うし ) 。

この文章の形態素を1単語ずつモデルに渡して次の単語を正しく予測するように学習していきます。

 

f:id:pytry3g:20180316200508p:plain

### Training ###
n_epochs = 1000
n_vocab = len(word2id)
embedding_dim = 128
hidden_dim = 128
learning_rate = 0.01

model = RNN(n_vocab, embedding_dim, hidden_dim)
if USE_CUDA:
    model.cuda()
criterion = nn.CrossEntropyLoss()
optimizer = O.Adam(model.parameters(), lr=learning_rate)

print("USE_CUDA: {}\nn_epochs: {}\nn_vocab: {}\n".format(USE_CUDA, n_epochs, n_vocab))

for epoch in range(n_epochs):
    if (epoch+1) % 100 == 0:
        print("Epoch {}".format(epoch+1))
    random.shuffle(id2sent)
    for indices in id2sent:
        model.init_hidden()
        model.zero_grad()
        source = variable(indices[:-1])
        target = variable(indices[1:])
        loss = 0
        for x, t in zip(source, target):
            y = model(x)
            loss += criterion(y, t)
        loss.backward()
        optimizer.step()

model_name = "example.model"
torch.save(model.state_dict(), model_name)

 

2000epochでcpuを使って学習をしました。ただ、自分のマシンではかなり時間がかかるのでFloydHubを使いました。学習時間は14時間ぐらいです。

古い情報かもしれませんが以前Qiitaに投稿した記事でFloydHubについて少し紹介しています。

qiita.com

 

5. 学習したモデルを使って文章の自動生成

今回は信長という単語を学習したモデルに渡して文章を自動生成します。

信長を含めて生成した文章が30語以下もしくは、。が出たら終了するようにします。

 

n_vocab = len(word2id)
embedding_dim = 128
hidden_dim = 128
learning_rate = 0.01
model = RNN(n_vocab, embedding_dim, hidden_dim)
id2word = {v: k for k, v in word2id.items()}
model.load_state_dict(torch.load("example.model"))
morpheme = "信長"
sentence = [morpheme]

### Test the model ###
for i in range(30):
    result = torch.max(model(var), 1)[1].data[0]
    morpheme = id2word[result]
    sentence.append(morpheme)
    if morpheme == "。":
        break
print("".join(sentence))

結果はこの様になりました。

信長自らの出陣で士気が高揚した織田軍は、光秀率いる天王寺砦の軍勢との連携・合流に成功。

すごいですね。まるで人間が書いた文章みたいです。が、ここで少し不安になったので織田信長Wikipediaページを見てみると、生成した文章とまったく同じ文章がありました。

これはいわゆる過学習ってやつですかね。

学習データを丸暗記するのが過学習だったはず。

なので、過学習を防ぐためにDropoutを入れて再度学習し直してみます。ついでに、gpuでも動くようにコードを変えてみます。

 

1000epochでgpuを使ってFloydHub上で学習し直しました。4時間20分かかりました。

gpuを使って学習したモデルは、gpuを使わないとエラーが出ます。当然か。

しかし、自分のパソコンはgpuは使えません。そこで、cpuでもモデルが使えようにします。やり方は、こんな感じです。

model_name = "example_gpu.model"
model.load_state_dict(torch.load(model_name, map_location=lambda storage, loc: storage))

これで学習したモデルをcpuで使えるようになります。

今回も信長という単語をモデルに渡して、文章を作ってみます。 

結果は、↓↓↓

信長は先頭に立って真っ先に撤退し、僅か名の兵と共に京に到着したという。

今回作成した文章もWikipediaそっくりそのままありました。

おわり

この記事を書く前は生成した文章に文法ミスがあったり何か面白い文章が出来るのかと思っていましたが、2000epochで学習したモデルと1000epochで過学習を防ぐためにDropoutを使ったモデル両方がちゃんとした文章を作ることができました。

単語をモデルに渡して次の単語を予測するというのを繰り返して学習しているから、Wikipediaにある文章と同じ文章ができてもおかしくないのかな。

少なくともでたらめな文章は生成されなかったので、たぶんやったことは間違ってなかったんだと思います。

 

最後にコードです。

github.com