色々めんどくさいので作りながら学ぶ自然言語処理(RNN)


自然言語処理とは何?

・自然言語は普段私たちが使っている言葉それをコンピュータで処理する技術を
 自然言語処理(natural language processing nlp)という

そして、実際何に使われてるの?

・検索エンジニン(キーワード検索とか...)
・機械翻訳
・予測変換
・メールファイター
・みんな大好きalexa

色んな技術

・形態素解析
→文章を単語に分割する技術
・word2vec
→文書内での関係性を踏まえて、単語をベクトルに変換
・リカレントニュートラルネットワーク
→時系列を扱うのが得意なニュートラルネットワーク
・seq2seq
→RNNをベースにした文章生成モデル
などなど...

建前は置いておいてとりあえず実装していきます。

今回は、文章の自動生成を行っていきます。
使うデータは宮沢賢治の「アメニモマケズ」をデータを使い(風であって宮沢賢治っぽい)文章を生成してみます。
文章における文字の並びを時系列データと捉えて、次の文字を予測するようにRNNを訓練します。
シンプルなRNN、LSTM、およびGRUの3つのRNNでそれぞれモデルを構築し、文章の生成結果を比較します。

テキストデータの読み込み

from google.colab import drive

drive.mount('/content/drive/')
nov_dir = 'omio/mko1/'  # フォルダへのパス
nov_path = '/content/drive/My Drive/' + nov_dir + 'qwe1.txt'

# ファイルを読み込む
with open(nov_path, 'r') as f:
  nov_text = f.read()
  print(nov_text[:300])  # 最初の300文字のみ表示
出力.
Drive already mounted at /content/drive/; to attempt to forcibly remount, call drive.mount("/content/drive/", force_remount=True).
雨ニモマケズ
風ニモマケズ
雪ニモ夏ノ暑サニモマケヌ
丈夫ナカラダヲモチ
慾ハナク
決シテ瞋ラズ
イツモシヅカニワラッテヰル
一日ニ玄米四合ト
味噌ト少シノ野菜ヲタベ
アラユルコトヲ
ジブンヲカンジョウニ入レズニ
ヨクミキキシワカリ
ソシテワスレズ
野原ノ松ノ林ノ
小サナ萓ブキノ小屋ニヰテ
東ニ病気ノコドモアレバ
行ッテ看病シテヤリ
西ニツカレタ母アレバ
行ッテソノ稲ノ朿ヲ[#「朿ヲ」はママ]負ヒ
南ニ死ニサウナ人アレバ
行ッテコハガラナクテモイヽトイヒ
北ニケンクヮヤソショウガアレバ
ツマラナイカラヤメロトイヒ
ヒドリノトキハナミダヲナガシ
サムサノナツハオロオロアルキ
ミンナニデクノボートヨバレ
ホメラレモセズ
クニモサレズ
サウイフモノニ
ワタシハナリタイ



続いて、余計なものを取り除いて正規表現による前処理を行います

import re  # 正規表現に必要なライブラリ

text = re.sub("《[^》]+》", "", nov_text) # ルビの削除
text = re.sub("[[^]]+]", "", text) # 読みの注意の削除
text = re.sub("[|  ]", "", text) # | と全角半角スペースの削除
print("文字数", len(text))  # len() で文字列の文字数も取得可能
出力.
文字数 327

RNNの設定

特に理由はないです。自由に設定してください

n_rnn = 10  # 時系列の数
batch_size = 128
epochs = 60
n_mid = 128  # 中間層のニューロン数

文字をベクトル化していきます。

各文字をone-hot表現で表し、時系列の入力データおよび正解データを作成します
RNNの最後のの時刻のみを出力し最後の出力に対応する正解だけ取ります。

xが入力 tが正解とします。

import numpy as np

# インデックスと文字で辞書を作成(後でonehotで使用するため)
chars = sorted(list(set(text)))  # setで文字の重複をなくし、各文字をリストに格納する
print("文字数(重複無し)", len(chars))
char_indices = {}  # 文字がキーでインデックスが値
for i, char in enumerate(chars):
    char_indices[char] = i
indices_char = {}  # インデックスがキーで文字が値
for i, char in enumerate(chars):
    indices_char[i] = char

# 時系列データと、それから予測すべき文字を取り出します
time_chars = []
next_chars = []
for i in range(0, len(text) - n_rnn):
    time_chars.append(text[i: i + n_rnn])
    next_chars.append(text[i + n_rnn])

# 入力と正解をone-hot表現で表します
x = np.zeros((len(time_chars), n_rnn, len(chars)), dtype=np.bool)
t = np.zeros((len(time_chars), len(chars)), dtype=np.bool)
for i, t_cs in enumerate(time_chars):
    t[i, char_indices[next_chars[i]]] = 1  # 正解をone-hot表現で表す
    for j, char in enumerate(t_cs):
        x[i, j, char_indices[char]] = 1  # 入力をone-hot表現で表す

print("xの形状", x.shape)
print("tの形状", t.shape)
出力.
文字数(重複無し) 103
xの形状 (317, 10, 103)
tの形状 (317, 103)

モデル構築

SimpleRNN、LSTM、GRUの層を使ったモデルをそれぞれ構築します
今回のアルゴリズムはadam使います。

from keras.models import Sequential
from keras.layers import Dense, SimpleRNN, LSTM, GRU

# SimpleRNN
model_rnn = Sequential()
model_rnn.add(SimpleRNN(n_mid, input_shape=(n_rnn, len(chars))))
model_rnn.add(Dense(len(chars), activation="softmax"))
model_rnn.compile(loss='categorical_crossentropy', optimizer="adam")
print(model_rnn.summary())

# LSTM
model_lstm = Sequential()
model_lstm.add(LSTM(n_mid, input_shape=(n_rnn, len(chars))))
model_lstm.add(Dense(len(chars), activation="softmax"))
model_lstm.compile(loss='categorical_crossentropy', optimizer="adam")
print(model_lstm.summary())

# GRU
model_gru = Sequential()
model_gru.add(GRU(n_mid, input_shape=(n_rnn, len(chars))))
model_gru.add(Dense(len(chars), activation="softmax"))
model_gru.compile(loss='categorical_crossentropy', optimizer="adam")
print(model_gru.summary())
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn (SimpleRNN)       (None, 128)               29696     
_________________________________________________________________
dense (Dense)                (None, 103)               13287     
=================================================================
Total params: 42,983
Trainable params: 42,983
Non-trainable params: 0
_________________________________________________________________
None
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
lstm (LSTM)                  (None, 128)               118784    
_________________________________________________________________
dense_1 (Dense)              (None, 103)               13287     
=================================================================
Total params: 132,071
Trainable params: 132,071
Non-trainable params: 0
_________________________________________________________________
None
Model: "sequential_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
gru (GRU)                    (None, 128)               89472     
_________________________________________________________________
dense_2 (Dense)              (None, 103)               13287     
=================================================================
Total params: 102,759
Trainable params: 102,759
Non-trainable params: 0
_________________________________________________________________
None

まあまあ時間かかりそうです....

文書生成

ここでは、文章を生成するための関数を書きます。
Lambdacallback使って設定していきます。

from keras.callbacks import LambdaCallback

def on_epoch_end(epoch, logs):
    print("エポック: ", epoch)

    beta = 5  # 確率分布を調整する定数
    prev_text = text[0:n_rnn]  # 入力に使う文字
    created_text = prev_text  # 生成されるテキスト

    print("シード: ", created_text)

    for i in range(400):
        # 入力をone-hot表現に
        x_pred = np.zeros((1, n_rnn, len(chars)))
        for j, char in enumerate(prev_text):
            x_pred[0, j, char_indices[char]] = 1

        # 予測を行い、次の文字を得る
        y = model.predict(x_pred)
        p_power = y[0] ** beta  # 確率分布の調整
        next_index = np.random.choice(len(p_power), p=p_power/np.sum(p_power))        
        next_char = indices_char[next_index]

        created_text += next_char
        prev_text = prev_text[1:] + next_char

    print(created_text)
    print()

# エポック終了後に実行される関数を設定
epock_end_callback= LambdaCallback(on_epoch_end=on_epoch_end)

学習実装

構築したモデルを使って、学習を行います。
fit( )メソッドをではコールバックの設定をし、エポック終了後に関数が呼ばれるようにします

#RNN
model = model_rnn
history_rnn = model_rnn.fit(x, t,
                    batch_size=batch_size,
                    epochs=epochs,
                    callbacks=[epock_end_callback])

*結果はかなり長いので省きます。
何となく自然な文章を構成していっていると確認ができます。以下その他のモデルもこんな結果が返ってきてます。

自動生成後.
Epoch 60/60
3/3 [==============================] - 0s 12ms/step - loss: 0.1554
エポック:  59
シード:  雨ニモマケズ
風ニモ
雨ニモマケズ
風ニモマケズ
雪ニモ夏ノ暑サニモマケヌ
丈夫ナカラダヲモチ
慾ハナク
決シテ瞋ラズ
イツモシヅカニワラッテヰル
一日ニ玄米四合ト
味噌ト少シノ野菜ヲタベ
アラユルコトヲ
ジブンヲカンジョウニ入レズニ
ヨクミキキシワカリ
ソシテワスレズ
野原ノ松ノ林ノ
小サナ萓ブキノ小屋ニヰテ
東ニ病気ノコドモアレバ
行ッテ看病シテヤリ
西ニツカレタ母アレバ
行ッテソノ稲ノ朿ヲ負ヒ
南ニ死ニサウナ人アレバ
行ッテコハガラナクテモイヽトイヒ
北ニケンクヮヤソショウガアレバ
ツマラナイカラヤメロトイヒ
ヒドリノトキハナミダヲナガシ
サムサノナツハオロオロアルキ
ミンナニデクノボートヨバレ
ホメラレモセズ
クニモサレズ
サウイフモノニ
ワタシハナリタイ

ズ

ム
キ
テニ
ワオオア
ウタカ

小ナナノ
ズ

アニキ

ヲ
サレタノ
ウ

ヲタテク
ラ
テラナツ
セシヒ米タア



サタシノノノ

ロオロ
ユ気気屋ニト
# LSTM
model = model_lstm
history_lstm = model_lstm.fit(x, t,
                    batch_size=batch_size,
                    epochs=epochs,
                    callbacks=[epock_end_callback])

*結果はかなり長いのですべて省きます。

# GRU
model = model_gru
history_gru = model_gru.fit(x, t,
                    batch_size=batch_size,
                    epochs=epochs,
                    callbacks=[epock_end_callback])

*結果はかなり長いのですべて省きます。

学習の精度

import matplotlib.pyplot as plt

loss_rnn = history_rnn.history['loss']
loss_lstm = history_lstm.history['loss']
loss_gru = history_gru.history['loss']

plt.plot(np.arange(len(loss_rnn)), loss_rnn, label="RNN")
plt.plot(np.arange(len(loss_lstm)), loss_lstm, label="LSTM")
plt.plot(np.arange(len(loss_gru)), loss_gru, label="GRU")
plt.legend()
plt.show()

学習は推移していないですがエポックを重ねればさらに効果がでてくると考えられます。
さらに精度を上げるなら入力をベクトル化していきonehotではなくword2vecなどでも良いです。
さらにさまざまな、アルゴリズムが存在するため取り入れていくのもよいと思います。
RNNを市場予測や自動作曲などに応用することも可能です。

感想

精度が高い自然言語処理を作っていこうかなと思います(気が向いたら)