NLP(自然言語処理):メモリに乗らない大きなデータを分割して訓練する

以前の記事,kerasとLSTMを用いて学習と予測を行うで,モデルを構築し,fit()を用いて訓練を行う方法を学びました。

ここでは,ジェネレーター関数を用いてデータを分割して訓練する方法を学びます。

LSTMに与える入力値は3階のテンソル(3次元の行列)であるため,大きなデータが作成されます。訓練に用いる文章が長くなると,データがメモリの許容量を超え,エラーを起こします。

そのため,データを分割して処理する必要があります。

ここでは,複数の文によって構成される文章を読み込み,10行の文を1つのデータセットとして訓練します。単語ベクトルは1次元とします。つまり,それぞれの単語を,それに対応する1つの数値で表します。モデルは5つの単語をもとに,それに続く1つの単語を予測します。

fit()とジェネレーター関数

訓練を行うとき,fit()に入力x, 答えyを直接与える方法と,ジェネレーター関数を呼び出す方法があります。

fit()x, yを直接与える場合,1つのデータセットしか訓練できません。

それに対して,ジェネレーター関数は訓練の間何度も呼び出され,異なったx, yfit()に返します。

model.fit(x, y, ......)
model.fit(train_generator(), ......)

ここでは,ジェネレーター関数にtrain_generator()という名前をつけました。

以前は,ジェネレーター関数を呼び出すためにfit_generator()を用いていましたが,現在ではfit()にジェネレーター関数を書くことができるようになったため,fit_generator()は非推奨とされています。新しいバージョンのTensorFlowでは警告が出ます。

パディング

texts = sequence.pad_sequences(texts, maxlen=30, padding="pre", truncating="post")

リストtextsには,複数の文のそれぞれの単語を数値で表したものが格納されています。それぞれの文は長さが異なります。しかし,ジェネレーター関数を呼び出す場合,リストの長さが揃っていなければなりません。そこで,文の前や後ろに0を挿入することで,リストの長さを揃えます。

texts =
[[ 1 2 3 4 ]
 [ 1 2 3 4 5 6 ]
 [ 1 2 ]]

---->

[[ 0 0 0 0 0 1 2 3 4 ]
 [ 0 0 0 1 2 3 4 5 6 ]
 [ 0 0 0 0 0 0 0 1 2 ]]

ここでは,maxlen=30として,一つの文が持つ単語の数を30個とします。

padding="pre"は文の前に0を挿入します。また,truncating="post"は,文が30語以上あるとき,文の後ろの方の単語をカットします。

パディングは文の前に0を挿入する場合と,文の後ろに0を挿入する場合があります。ここでは5つの単語をもとに,それに続く1つの単語を予測します。もし,文の後ろに0を挿入すると,5つの単語をもとに0を予測するパターンが作られます。これは単語を予測しないことと同じなので,好ましくありません。

結論として,文の前に0を挿入すべきです。

ジェネレーター関数

def train_generator():

ジェネレーター関数train_generator()を定義します。

ジェネレーターが生成するバッチデータの大きさは,必要に応じて決定されます。ここでは,一つの事例を示します。

    while True:

fit()はジェネレーター関数を何度も呼び出します。while True:は無限にループし,データセットを返します。ここでは,繰り返しの回数はエポック数と等しくなります。

        for step in range(len(texts)//batch_size):

batch_size は 10 とします。つまり,10行の文を1つのバッチとしてみなします。1行の文はパディングによって30個の固定した長さのデータで構成されています。len(texts)は文章が持つ行数を表し,batch_sizeで割ります。//は小数点以下を切り捨てて割り算します。

例えば,218行の文があるなら,218 // 10 = 21 となります。つまり,全体のデータを21個のステップに分割するということです。最後の8行はカットされます。したがって,for文は21回繰り返し処理します。

            for line in range(batch_size):

batch_size=10なので,for文は10回繰り返し処理します。

                dataset = TimeseriesGenerator(
                    texts[step*batch_size+line],
                    texts[step*batch_size+line],
                    length=seq_length,
                    batch_size=1)
                for batch in dataset:
                    X, Y = batch
                    x.extend(X[0])
                    y.extend(Y)

TimeseriesGenerator()は時系列データを生成します。詳しくは,以前の記事を参照してください。

            x = np.reshape(x,(25*batch_size,seq_length,1))

データを3階のテンソルに変換します。それぞれの文は30個のデータで構成されます。それに基づいて生成されるデータは25個です。10行のデータは5*25*10=1250個あるので,それを(250,5,1)のリストに変換します。

[ 1 2 3 4 5 .... 30]

X = [  1  2  3  4  5 ]  Y = [6]
    [  2  3  4  5  6 ]      [7]
    [  3  4  5  6  7 ]      [8]

    ......             ......

    [ 25 26 27 28 29 ]      [30]

reshape --->

x = [[[1]
      [2]
      [3]
      [4]
      [5]]
     [[2]
      [3]
      [4]
      [5]
      [6]]
     [[3]
      [4]
      [5]
      [6]
      [7]]

     ......

     [[25]
      [26]
      [27]
      [28]
      [29]]]
            x = x / float(len(char_indices)+1)

値を語彙数で割り,0から1の間の値に正規化します。

            y = np_utils.to_categorical(y, len(char_indices)+1)

yをone-hot形式に変換します。

            yield x, y

xyfit()に返します。一般的に,関数が値を返すときにはreturnを使います。returnを使う場合,再び関数を呼び出すと繰り返しの最初に戻ります。しかし,yieldの場合は,繰り返しの途中から処理を継続します。

ジェネレーター関数は,繰り返し処理に基づいて,(250,5,1)のリストをfit()に返し,fit()はそれに基づいて訓練を行います。fit()はすべてのデータセットを訓練し,1エポックが終了します。

訓練

model.fit(
    train_generator(),
    steps_per_epoch=len(texts) // batch_size,
    epochs=100,
    verbose=1)

fit()を用いてtrain_generator()を呼び出し,訓練を行います。steps_per_epochは何回train_generator()を呼び出すかを表しています。上で述べたように,ここではジェネレーター関数を21回呼び出します。

一般的に,バッチサイズを大きくするほど,処理速度は速くなります。しかし,大きなバッチサイズはそれだけ大きなメモリを必要とすることになります。

全体のコードを示します。

import numpy as np
import sys
import io
import os
os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
from tensorflow import keras
from keras.models import Sequential
#from keras.models import Model
from keras.layers import Embedding, Dense, LSTM
from keras.optimizers import Adam
from keras.utils import np_utils
from keras.preprocessing import sequence
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import TimeseriesGenerator
#read the text
with io.open('articles.txt', encoding='utf-8') as f:
    text = f.read()
texts = text.replace('eos', 'eos\n').splitlines()
#make the dictionary
tokenizer = Tokenizer()
tokenizer.fit_on_texts(texts)
char_indices = tokenizer.word_index
#make the inverted dictionary
indices_char = dict([(value, key) for (key, value) in char_indices.items()])
np.save('voa_char_indices', char_indices)
np.save('voa_indices_char', indices_char)
#vectorization
texts = tokenizer.texts_to_sequences(texts)
texts = sequence.pad_sequences(texts, maxlen=30, padding="pre", truncating="post")
#make dataset
batch_size = 10
seq_length = 5
def train_generator():
    while True:
        for step in range(len(texts)//batch_size):
            x = []
            y = []
            for line in range(batch_size):
                dataset = TimeseriesGenerator(
                    texts[step*batch_size+line],
                    texts[step*batch_size+line],
                    length=seq_length,
                    batch_size=1)
                for batch in dataset:
                    X, Y = batch
                    x.extend(X[0])
                    y.extend(Y)
            x = np.reshape(x,(25*batch_size,seq_length,1))
            x = x / float(len(char_indices)+1)
            y = np_utils.to_categorical(y, len(char_indices)+1)
            yield x, y
#build the model
print('build the model....')
model = Sequential()
model.add(LSTM(128,input_shape=(seq_length, 1)))
model.add(Dense(len(char_indices)+1, activation='softmax'))
optimizer = Adam(lr=0.01)
model.compile(loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy'])
#training
model.fit(
    train_generator(),
    steps_per_epoch=len(texts) // batch_size,
    epochs=100,
    verbose=1)
#save the model
model.save('model_voa.h5')

予測

訓練されたモデルを用いて,予測を行います。およそ30KBのテキストファイルから文を生成します。

the european union eu started sending millions of dollars services aid australia’s for teams raised .
the money came with promises to improve migrant of centers state lawmaker will coast .
the centers are paid for millennials people did horrible divide a single countries .

連続して予測することで,文を生成しました。しかし,意味のある文を生成することには失敗しています。

the european union eu started .
the money came with promises .
the centers are paid for the modern .

与えるデータをおよそ500KBに増やした結果です。文が短くなりました。これは,データに文末を示す記号 eos が含まれているためです。データの中に eos が出現する回数が増えたため,モデルは高い確率で eos を予測するようになったのです。このように,高い頻度で出現する単語をストップワードと呼びます。

model.add(LSTM(512,input_shape=(seq_length, 1)))
 ---->

the european union eu started sending millions this dollars .
the money came with promises .
the centers are paid for own .

LSTMのレイヤーを512に増やした結果です。あまり変化はありません。

訓練に用いるデータを大きくした結果,新たな問題が発生しました。

全体のコードを示します。

import numpy as np
import sys
import io
import os
os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
from keras.models import load_model
from keras.utils import np_utils
from tensorflow.keras.preprocessing.text import Tokenizer
#read the text
with io.open('articles.txt', encoding='utf-8') as f:
#with io.open('voacorpus/asitis01.txt', encoding='utf-8') as f:
    text = f.read()
texts = text.replace('eos', 'eos\n').splitlines()
tokenizer = Tokenizer()
tokenizer.fit_on_texts(texts)
char_indices = np.load('voa_char_indices.npy', allow_pickle=True).tolist()
indices_char = np.load('voa_indices_char.npy', allow_pickle=True).tolist()
indices_char[0] = '<null>'
pre_vec = tokenizer.texts_to_sequences(texts)
texts_vec = []
for line in range(len(pre_vec)):
    if len(pre_vec[line]) > 9:
        texts_vec.append(pre_vec[line])
line = 0
#load the model
print('load the model....')
model = load_model('model_voa.h5')
#prediction
x = np.zeros(5)
for line in range(10):
    chars = ''
    for i in range(30):
        if i == 0:
            for j in range(5):
                x[j] = texts_vec[line][j]
                chars += indices_char[x[j]] + ' '
        else:
            for j in range(4):
                x[j] = x[j+1]
            x[4] = index
        x_pred = np.reshape(x,[1, 5, 1])
        x_pred = x_pred / float(len(char_indices)+1)
        prediction = model.predict(x_pred)
        index = np.argmax(prediction)
        result = indices_char[index] + ' '
        chars += result
        if indices_char[index] == 'eos':
            break
    chars = chars.replace('eos', '.')
    print(chars)