NLPのためのPython:Kerasによるディープラーニングによるテキスト生成

Python for NLPの連載は今回で21回目です。

前回は、FacebookのFastTextライブラリを使用して、意味的な類似性を見つけ、テキスト分類を行う方法を説明しました。

今回は、PythonでKerasライブラリを使い、ディープラーニングの手法でテキストを生成する方法を紹介します。

テキスト生成は、NLPの最先端アプリケーションの1つです。

深層学習技術は、詩の作成、映画のスクリプトの生成、さらには作曲など、さまざまなテキスト生成タスクに利用されています。

しかし、今回は、入力された単語列が与えられたら、次の単語を予測するという、非常にシンプルなテキスト生成の例を見ていきます。

シェイクスピアの有名な小説「マクベス」の生テキストを使い、入力された単語列から次の単語を予測することにします。

この記事を読み終えたら、好きなデータセットを使ってテキスト生成を行えるようになります。

では、さっそく始めましょう。

ライブラリとデータセットのインポート

まず、この記事のスクリプトを実行するために必要なライブラリと、データセットをインポートします。

以下のコードでは、必要なライブラリをインポートしています。

import numpy as np
from keras.models import Sequential, load_model
from keras.layers import Dense, Embedding, LSTM, Dropout
from keras.utils import to_categorical
from random import randint
import re


次に、データセットをダウンロードする。

データセットのダウンロードには、PythonのNLTKライブラリを使用します。

Gutenberg Datasetは、シェイクスピアの「マクベス」を含む、142人の著者によって書かれた3036冊の英語の本が収録されているデータセットを使用します。

以下のスクリプトは、Gutenbergデータセットをダウンロードし、データセット内の全ファイルの名前を表示するものである。

import nltk
nltk.download('gutenberg')
from nltk.corpus import gutenberg as gut


print(gut.fileids())


次のような出力が得られるはずである。

['austen-emma.txt', 'austen-persuasion.txt', 'austen-sense.txt', 'bible-kjv.txt', 'blake-poems.txt', 'bryant-stories.txt', 'burgess-busterbrown.txt', 'carroll-alice.txt', 'chesterton-ball.txt', 'chesterton-brown.txt', 'chesterton-thursday.txt', 'edgeworth-parents.txt', 'melville-moby_dick.txt', 'milton-paradise.txt', 'shakespeare-caesar.txt', 'shakespeare-hamlet.txt', 'shakespeare-macbeth.txt', 'whitman-leaves.txt']


ファイルshakespeare-macbeth.txtは小説「マクベス」の生テキストである。

このファイルからテキストを読み込むには、gutenberg クラスの raw メソッドを利用することができる。

macbeth_text = nltk.corpus.gutenberg.raw('shakespeare-macbeth.txt')


データセットから最初の 500 文字を表示してみましょう。

print(macbeth_text[:500])


出力結果は以下の通りです。

[The Tragedie of Macbeth by William Shakespeare 1603]


Actus Primus. Scoena Prima.


Thunder and Lightning. Enter three Witches.


1. When shall we three meet againe?
In Thunder, Lightning, or in Raine?
  2. When the Hurley-burley's done,
When the Battaile's lost, and wonne


3. That will be ere the set of Sunne


1. Where the place?
  2. Vpon the Heath


3. There to meet with Macbeth


1. I come, Gray-Malkin


All. Padock calls anon: faire is foule, and foule is faire,
Houer through


多くの特殊文字や数字が含まれていることがわかるだろう。

次のステップはデータセットをきれいにすることである。

データ前処理

句読点や特殊文字を除去するために、preprocess_text() という名前の関数を定義する。

def preprocess_text(sen):
    # Remove punctuations and numbers
    sentence = re.sub('[^a-zA-Z]', ' ', sen)


# Single character removal
    sentence = re.sub(r"s+[a-zA-Z]s+", ' ', sentence)


# Removing multiple spaces
    sentence = re.sub(r's+', ' ', sentence)


return sentence.lower()


関数 preprocess_text は、パラメータとしてテキスト文字列を受け取り、小文字に変換されたクリーンなテキスト文字列を返します。

それでは、テキストをクリーニングして、最初の 500 文字をもう一度表示してみましょう。

macbeth_text = preprocess_text(macbeth_text)
macbeth_text[:500]


以下はその出力です。

the tragedie of macbeth by william shakespeare actus primus scoena prima thunder and lightning enter three witches when shall we three meet againe in thunder lightning or in raine when the hurley burley done when the battaile lost and wonne that will be ere the set of sunne where the place vpon the heath there to meet with macbeth come gray malkin all padock calls anon faire is foule and foule is faire houer through the fogge and filthie ayre exeunt scena secunda alarum within enter king malcom


単語を数字に変換する

ディープラーニングモデルは、統計的なアルゴリズムに基づいている。

したがって、ディープラーニングモデルを扱うためには、単語を数値に変換する必要があります。

この記事では、単語を1つの整数に変換する非常にシンプルなアプローチを使用する予定です。

単語を整数に変換する前に、テキストを個々の単語にトークン化する必要がある。

これには nltk.tokenize モジュールの word_tokenize() メソッドを使用します。

次のスクリプトはデータセットのテキストをトークン化し、データセット内の単語の総数とユニークワードの総数を表示します。

from nltk.tokenize import word_tokenize


macbeth_text_words = (word_tokenize(macbeth_text))
n_words = len(macbeth_text_words)
unique_words = len(set(macbeth_text_words))


print('Total Words: %d' % n_words)
print('Unique Words: %d' % unique_words)


出力は次のようになります。

Total Words: 17250
Unique Words: 3436


このテキストには全部で17250の単語があり、そのうち3436の単語がユニークであることがわかる。

トークン化された単語を数値に変換するには、 keras.preprocessing.text モジュールの Tokenizer クラスを使用します。

fit_on_texts` メソッドを呼び出して、単語のリストを渡す必要があります。

辞書が作成され、キーが単語を表し、整数がその辞書の対応する値を表します。

次のスクリプトを見てください。

from keras.preprocessing.text import Tokenizer
tokenizer = Tokenizer(num_words=3437)
tokenizer.fit_on_texts(macbeth_text_words)


単語とそれに対応するインデックスを含む辞書にアクセスするには、トークナイザーオブジェクトの word_index 属性を使用します。

vocab_size = len(tokenizer.word_index) + 1
word_2_index = tokenizer.word_index


辞書の長さを調べると、3436個の単語が含まれていることがわかる。

これは、今回のデータセットに含まれるユニークな単語の総数である。

それでは、500個目の単語を word_2_index 辞書から整数値で表示してみましょう。

print(macbeth_text_words[500])
print(word_2_index[macbeth_text_words[500]])


出力結果は以下の通りです。

comparisons
1456


ここでは、”comparisons “という単語に1456という整数値が割り当てられている。

データの形状を変更する

テキスト生成は、入力が単語の列、出力が1つの単語であるため、多対一列問題のカテゴリに属する。

テキスト生成のモデルには、リカレントニューラルネットワークの一種であるLSTM(Long Short-Term Memory Network)を使用する予定である。

LSTMは3次元形式(サンプル数、タイムステップ数、タイムステップごとの特徴量)でデータを受け取ります。

出力は1つの単語であるため、出力の形状は2次元(サンプル数、コーパスに含まれるユニークな単語の数)となる。

以下のスクリプトは、入力配列とそれに対応する出力の形状を変更する。

input_sequence = []
output_words = []
input_seq_length = 100


for i in range(0, n_words - input_seq_length , 1):
    in_seq = macbeth_text_words[i:i + input_seq_length]
    out_seq = macbeth_text_words[i + input_seq_length]
    input_sequence.append([word_2_index[word] for word in in_seq])
    output_words.append(word_2_index[out_seq])


上のスクリプトでは、2つの空のリスト input_sequenceoutput_words を宣言する。

input_seq_lengthは 100 にセットされ、これは入力シーケンスが 100 個の単語から構成されることを意味する。

次に、ループを実行します。

最初の反復では、テキストの最初の 100 個の単語に対する整数値がinput_sequenceリストに追加されます。

101番目の単語はoutput_wordsリストに追加される。

2回目の繰り返しでは、テキストの2番目の単語から始まり、101番目の単語で終わるシーケンスがinput_sequenceリストに格納され、102番目の単語がoutput_words` 配列に格納され、以下同様である。

データセットの総単語数が17250語なので、合計17150個の入力シーケンスが生成されることになる(総単語数より100個少ない)。

それでは、input_sequence リストの最初のシーケンスの値を表示してみよう。

print(input_sequence[0])


出力してみましょう。

[1, 869, 4, 40, 60, 1358, 1359, 408, 1360, 1361, 409, 265, 2, 870, 31, 190, 291, 76, 36, 30, 190, 327, 128, 8, 265, 870, 83, 8, 1362, 76, 1, 1363, 1364, 86, 76, 1, 1365, 354, 2, 871, 5, 34, 14, 168, 1, 292, 4, 649, 77, 1, 220, 41, 1, 872, 53, 3, 327, 12, 40, 52, 1366, 1367, 25, 1368, 873, 328, 355, 9, 410, 2, 410, 9, 355, 1369, 356, 1, 1370, 2, 874, 169, 103, 127, 411, 357, 149, 31, 51, 1371, 329, 107, 12, 358, 412, 875, 1372, 51, 20, 170, 92, 9]


入力配列を正規化するために、配列中の整数を最大の整数値で割ってみましょう。

また、以下のスクリプトは出力を2次元に変換する。

出力:“`
X = np.reshape(input_sequence, (len(input_sequence), input_seq_length, 1))
X = X / float(vocab_size)

y = to_categorical(output_words)


次のスクリプトは入力とそれに対応する出力の形状を表示する。

print(“X shape:”, X.shape)
print(“y shape:”, y.shape)


出力する。

X shape: (17150, 100, 1)
y shape: (17150, 3437)




### モデルのトレーニング

次のステップは、モデルを訓練することである。モデルの訓練に使用する層とニューロンの数について、厳密なルールはありません。ここでは、層とニューロンのサイズをランダムに選択することにします。より良い結果が得られるかどうか、ハイパーパラメータを弄ってみるのも良いでしょう。

我々はそれぞれ800ニューロンで3つのLSTM層を作成します。最後の密な層は1ニューロンで、次の単語のインデックスを予測するために追加される。

model = Sequential()
model.add(LSTM(800, input_shape=(X.shape[1], X.shape[2]), return_sequences=True))
model.add(LSTM(800, return_sequences=True))
model.add(LSTM(800))
model.add(Dense(y.shape[1], activation=’softmax’))

model.summary()

model.compile(loss=’categorical_crossentropy’, optimizer=’adam’)


出力される単語は3436個の単語のうちの1つであるため、我々の問題は多クラス分類問題であり、そのため損失関数 `categorical_crossentropy` が用いられる。バイナリ分類の場合は、`binary_crossentropy`関数が使われる。上記のスクリプトを実行すると、モデルの概要が表示されます。

Model: “sequential_1”


Layer (type) Output Shape Param #=================================================================
lstm_1 (LSTM) (None, 100, 800) 2566400


lstm_2 (LSTM) (None, 100, 800) 5123200


lstm_3 (LSTM) (None, 800) 5123200


dense_1 (Dense) (None, 3437) 2753037

Total params: 15,565,837
Trainable params: 15,565,837
Non-trainable params: 0


モデルを学習するには、単純に `fit()` メソッドを使えばよい。

model.fit(X, y, batch_size=64, epochs=10, verbose=1)


ここでも `batch_size` と `epochs` にいろいろな値を設定して遊ぶことができます。モデルの学習には多少時間がかかります。



### 予想すること

予測を行うために、`input_sequence` リストからランダムにシーケンスを選択し、3 次元形状に変換した後、学習済みモデルの `predict()` メソッドに渡します。このモデルは、1を含むインデックスが次の単語のインデックス値となる、ワンホットエンコードされた配列を返します。このインデックス値は `index_2_word` 辞書に渡され、単語のインデックスがキーとして利用されます。index_2_word` 辞書は、辞書のキーとして渡されたインデックスに属する単語を返す。

以下のスクリプトは、ランダムに整数の列を選択し、それに対応する単語の列を表示する。

random_seq_index = np.random.randint(0, len(input_sequence)-1)
random_seq = input_sequence[random_seq_index]

index_2_word = dict(map(reversed, word_2_index.items()))

word_sequence = [index_2_word[value] for value in random_seq]

print(‘ ‘.join(word_sequence))


この記事のスクリプトでは、次のような数列がランダムに選ばれている。あなたのために生成される配列は、これとは異なる可能性が高い。

amen when they did say god blesse vs lady consider it not so deepely mac but wherefore could not pronounce amen had most need of blessing and amen stuck in my throat lady these deeds must not be thought after these wayes so it will make vs mad macb me thought heard voyce cry sleep no more macbeth does murther sleepe the innocent sleepe sleepe that knits vp the rauel sleeue of care the death of each dayes life sore labors bath balme of hurt mindes great natures second course chiefe nourisher in life feast lady what doe you meane


上のスクリプトでは、`index_2_word`辞書は `word_2_index` 辞書を単純に反転して作成されている。この場合、辞書を反転させるとは、キーと値を入れ替える処理を指す。

次に、上記の単語の並びの後に続く100個の単語を表示する。

for i in range(100):
int_sample = np.reshape(random_seq, (1, len(random_seq), 1))
int_sample = int_sample / float(vocab_size)

predicted_word_index = model.predict(int_sample, verbose=0)

predicted_word_id = np.argmax(predicted_word_index)
seq_in = [index_2_word[index] for index in random_seq]

word_sequence.append(index_2_word[ predicted_word_id])

random_seq.append(predicted_word_id)
random_seq = random_seq[1:len(random_seq)]


変数 `word_sequence` には、入力された単語のシーケンスと、次に予測される100個の単語が格納されます。word_sequence` 変数には、リスト形式の単語のシーケンスが格納されています。以下のように、リスト内の単語を単純に結合して、最終的な出力シーケンスを得ることができます。

final_output = “”
for word in word_sequence:
final_output = final_output + ” ” + word

print(final_output)


以下は最終的な出力である。

amen when they did say god blesse vs lady consider it not so deepely mac but wherefore could not pronounce amen had most need of blessing and amen stuck in my throat lady these deeds must not be thought after these wayes so it will make vs mad macb me thought heard voyce cry sleep no more macbeth does murther sleepe the innocent sleepe sleepe that knits vp the rauel sleeue of care the death of each dayes life sore labors bath balme of hurt mindes great natures second course chiefe nourisher in life feast lady what doe you meane and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and and

“`

この出力はまだあまり良くは見えません。

このモデルは最後の単語、つまり「and」からしか学習していないようです。

しかし、Kerasでテキスト生成モデルを作成する方法についてはご理解いただけたと思います。

結果を改善するために、私はあなたに以下の推奨事項があります。

  • LSTMのレイヤーのサイズと数、エポック数を含むハイパーパラメータを変更して、より良い結果が得られるかどうかを確認します。
  • テストセットでストップワード以外の単語を生成するために、学習セットから is, am, are のようなストップワードを削除してみる(これはアプリケーションの種類によりますが)。
  • 次の N 文字を予測する文字レベルのテキスト生成モデルを作成する。

さらに練習するために、Gutenbergコーパスの他のデータセットでテキスト生成モデルを開発してみることをお勧めします。

結論

今回は、PythonのKerasライブラリを使ったディープラーニングによるテキスト生成モデルの作り方を見てきました。

この記事で開発したモデルは完璧ではありませんが、ディープラーニングでテキストを生成する方法の考え方が伝わりました。

タイトルとURLをコピーしました