ディープラーニングで自動筆記 − Kerasを用いた文書生成(前編):ディープラーニング習得、次の一歩(2/3 ページ)
ディープラーニングによる自然言語処理の一つ「文書生成」にチャレンジしてみよう。ネットワークにLSTM、ライブラリにKeras+TensorFlowを採用し、徐々に精度を改善していくステップを説明する。
ニューラルネットワーク構築
ニューラルネットはKerasのSequentialモデルを用いて構築する。
学習データは、各単語のインデックス、すなわち整数の列になっている。これをOne-hotベクトル(ある1つの次元だけが1で、その他が0のベクトル。今回の例では、インデックスの数字に相当する次元を1にする)の列に変換したものが、ニューラルネットの入力になる。
処理の流れは、
(1)入力のOne-hotベクトルを実数ベクトル空間に埋め込む(Embeddingレイヤー)
(2)バッチ正規化(BatchNormalizationレイヤー)
(3)入力の欠損用に0を予約する(Maskingレイヤー)
(4)LSTMレイヤー
(5)バッチ正規化(BatchNormalizationレイヤー)
(6)ドロップアウトレイヤー
(7)出力次元へ変換(Denseレイヤー)
(8)活性化(Activationレイヤー)
となる。
バッチ正規化というのは、平均0、分散1になるようにパラメーターを変換する処理で、これをやると学習が速く進むという触れ込みであるが、確かに目に見えて効果がある。
create_modelメソッドでインスタンスを生成し、trainメソッドで学習を実行する。学習の発散を防ぐため、trainメソッド内でEarly-Stopping(=学習を適切なタイミングで早期に打ち切る機能)を使用する。
各パラメーターの初期値は乱数で与えられるが、これが試行のたびに毎回同じになるように、乱数のseed指定を行う。
trainメソッド内のfitのパラメーターvalidation_splitに0.1が指定されているが、これは入力データの10%を評価用に使用するという意味である。
class Prediction :
def __init__(self, maxlen, n_hidden, input_dim, vec_dim, output_dim):
self.maxlen = maxlen
self.n_hidden = n_hidden
self.input_dim = input_dim
self.vec_dim = vec_dim
self.output_dim = output_dim
def create_model(self):
model = Sequential()
print('#3')
model.add(Embedding(self.input_dim, self.vec_dim, input_length=self.maxlen,
embeddings_initializer=uniform(seed=20170719)))
model.add(BatchNormalization(axis=-1))
print('#4')
model.add(Masking(mask_value=0, input_shape=(self.maxlen, self.vec_dim)))
model.add(LSTM(self.n_hidden, batch_input_shape=(None, self.maxlen, self.vec_dim),
activation='tanh', recurrent_activation='hard_sigmoid',
kernel_initializer=glorot_uniform(seed=20170719),
recurrent_initializer=orthogonal(gain=1.0, seed=20170719),
dropout=0.5,
recurrent_dropout=0.5))
print('#5')
model.add(BatchNormalization(axis=-1))
model.add(Dropout(0.5, noise_shape=None, seed=None))
print('#6')
model.add(Dense(self.output_dim, activation=None, use_bias=True,
kernel_initializer=glorot_uniform(seed=20170719),
))
model.add(Activation("softmax"))
model.compile(loss="categorical_crossentropy", optimizer="RMSprop", metrics=['categorical_accuracy'])
return model
# 学習
def train(self, x_train, t_train, batch_size, epochs) :
early_stopping = EarlyStopping(patience=1, verbose=1)
print('#2', t_train.shape)
model = self.create_model()
print('#7')
model.fit(x_train, t_train, batch_size=batch_size, epochs=epochs, verbose=1,
shuffle=True, callbacks=[early_stopping], validation_split=0.1)
return model
メイン処理
上記で定義したニューラルネットを動かして、学習を進める。入力次元数input_dimは単語数である。1を足しているのは、マスキング用に0を予約してあるからである。LSTMへの入力次元vec_dimは400、LSTMからの出力次元はその1.5倍にしてある。あまり欲張って大きな値にすると、処理が遅くなったり、メモリが足りなくなったりするので、いろいろ試した結果、これくらいの値にした。
trainメソッドのラベルデータ引数のところに現れるnp_utils.to_categoricalというのは、入力の数字をOne-hotベクトルに変換してくれるという、優れものである。
なお、学習と文書生成を別のタイミングで実施できるように、学習用のニューラルネットと、文書生成用のニューラルネットは分けることにした。文書生成用ニューラルネットで使えるように、学習済みのパラメーターをセーブしてある。
vec_dim = 400
epochs = 100
batch_size = 200
input_dim = len(words)+1
output_dim = input_dim
n_hidden = int(vec_dim*1.5) # 隠れ層の次元
prediction = Prediction(maxlen, n_hidden, input_dim, vec_dim, output_dim)
emb_param = 'param_1.hdf5' # パラメーターファイル名
row = x_train.shape[0]
x_train = x_train.reshape(row, maxlen)
model = prediction.train(x_train, np_utils.to_categorical(t_train, output_dim), batch_size, epochs)
model.save_weights(emb_param) # 学習済みパラメーターセーブ
row2 = x_validation.shape[0]
score = model.evaluate(x_validation.reshape(row2, maxlen),
np_utils.to_categorical(t_validation, output_dim), batch_size=batch_size, verbose=1)
print("score:", score)
以上、ここまで、以下の順でコードを実行してきた。
- リスト1
- リスト2
- リスト3
- リスト4
- リスト5
- リスト6
学習と改善
上記のニューラルネットを使って学習を進めてみたが、正解率は3割に満たなかった。そこで汎化性能はとりあえずあきらめて、全データを学習用に投入することにした。
そのためのコードを以下に示す。
まず、訓練データ作成である。データを訓練用とテスト用に分けるのをやめている。
maxlen = 40 # 入力語数
mat_urtext = np.zeros((len(mat), 1), dtype=int)
for i in range(0, len(mat)):
#row = np.zeros(len(words), dtype=np.float32)
if mat[i] in word_indices : # 出現頻度の低い単語のインデックスをunkのそれに置き換え
if word_indices[mat[i]] != 0 : # 0パディング対策
mat_urtext[i, 0] = word_indices[mat[i]]
else :
mat_urtext[i, 0] = len(words)
else:
mat_urtext[i, 0] = word_indices['UNK']
print(mat_urtext.shape)
# 単語の出現数をもう一度カウント:UNK置き換えでwords_indeicesが変わっているため
cnt = np.zeros(len(words)+1)
for j in range(0, len(mat)):
cnt[mat_urtext[j, 0]] += 1
print(cnt.shape)
len_seq = len(mat_urtext)-maxlen
data = []
target = []
for i in range(0, len_seq):
data.append(mat_urtext[i:i+maxlen, :])
target.append(mat_urtext[i+maxlen, :])
x = np.array(data).reshape(len(data), maxlen, 1)
t = np.array(target).reshape(len(data), 1)
z = list(zip(x, t))
nr.shuffle(z) # シャッフル
x, t = zip(*z)
x_train = np.array(x).reshape(len(data), maxlen, 1)
t_train = np.array(t).reshape(len(data), 1)
print(x_train.shape, t_train.shape)
次にニューラルネット定義である。変わっているのは、validation_splitが0.0になっているところである。
class Prediction :
def __init__(self, maxlen, n_hidden, input_dim, vec_dim, output_dim):
self.maxlen = maxlen
self.n_hidden = n_hidden
self.input_dim = input_dim
self.vec_dim = vec_dim
self.output_dim = output_dim
def create_model(self):
model = Sequential()
print('#3')
model.add(Embedding(self.input_dim, self.vec_dim, input_length=self.maxlen,
embeddings_initializer=uniform(seed=20170719)))
model.add(BatchNormalization(axis=-1))
print('#4')
model.add(Masking(mask_value=0, input_shape=(self.maxlen, self.vec_dim)))
model.add(LSTM(self.n_hidden, batch_input_shape=(None, self.maxlen, self.vec_dim),
activation='tanh', recurrent_activation='hard_sigmoid',
kernel_initializer=glorot_uniform(seed=20170719),
recurrent_initializer=orthogonal(gain=1.0, seed=20170719),
dropout=0.5,
recurrent_dropout=0.5))
print('#5')
model.add(BatchNormalization(axis=-1))
model.add(Dropout(0.5, noise_shape=None, seed=None))
print('#6')
model.add(Dense(self.output_dim, activation=None, use_bias=True,
kernel_initializer=glorot_uniform(seed=20170719),
))
model.add(Activation("softmax"))
model.compile(loss="categorical_crossentropy", optimizer="RMSprop", metrics=['categorical_accuracy'])
return model
# 学習
def train(self, x_train, t_train, batch_size, epochs) :
early_stopping = EarlyStopping(monitor='loss', patience=1, verbose=1)
print('#2', t_train.shape)
model = self.create_model()
print('#7')
model.fit(x_train, t_train, batch_size=batch_size, epochs=epochs, verbose=1,
shuffle=True, callbacks=[early_stopping], validation_split=0.0)
return model
最後にメイン処理である。スコア算出時に指定する学習データ、ラベルデータが変わっている。
vec_dim = 400
epochs = 100
batch_size = 200
input_dim = len(words)+1
output_dim = input_dim
n_hidden = int(vec_dim*1.5) # 隠れ層の次元
prediction = Prediction(maxlen, n_hidden, input_dim, vec_dim, output_dim)
emb_param = 'param_1.hdf5' # パラメーターファイル名
row = x_train.shape[0]
x_train = x_train.reshape(row, maxlen)
model = prediction.train(x_train, np_utils.to_categorical(t_train, output_dim), batch_size, epochs)
model.save_weights(emb_param) # 学習済みパラメーターセーブ
score = model.evaluate(x_train,
np_utils.to_categorical(t_train, output_dim), batch_size=batch_size, verbose=1)
print("score:", score)
ちなみに、コードの実行順をまとめると以下のとおりになる。
- リスト1
- リスト2
- リスト3 (ここまで共通)
- リスト4-1
- リスト5-1
- リスト6-1
以上のコードで学習を実施したところ、正解率は30%程度になった。
文書生成
正解率の改善は期待したほどではなかったが、この状態でどの程度のレベルの文書生成ができるか、文書生成用のニューラルネットを構築して試してみることにする。
リスト4-1を、以下に入れ替える。訓練データ作成処理とほとんど同じだが、本処理は文書生成の初期値を入手するためのものであり、訓練に使用するわけではないので、シャッフルの処理がなくなっている。
maxlen = 40 # 入力語数
mat_urtext = np.zeros((len(mat), 1), dtype=int)
for i in range(0, len(mat)):
#row = np.zeros(len(words), dtype=np.float32)
if mat[i] in word_indices : # 出現頻度の低い単語のインデックスをUNKのそれに置き換え
if word_indices[mat[i]] != 0 : # 0パディング対策
mat_urtext[i, 0] = word_indices[mat[i]]
else :
mat_urtext[i, 0] = len(words)
else:
mat_urtext[i, 0] = word_indices['UNK']
print(mat_urtext.shape)
# 単語の出現数をもう一度カウント:UNK置き換えでwords_indeicesが変わっているため
cnt = np.zeros(len(words)+1)
for j in range(0, len(mat)):
cnt[mat_urtext[j, 0]] += 1
print(cnt.shape)
data = []
target = []
len_seq = len(mat_urtext)-maxlen
for i in range(0, len_seq):
# 単語
data.append(mat_urtext[i:i+maxlen, :])
target.append(mat_urtext[i+maxlen, :])
x_train = np.array(data).reshape(len(data), maxlen, 1)
t_train = np.array(target).reshape(len(data), 1)
print(x_train.shape, t_train.shape)
また、ニューラルネットの活性化と、学習済みパラメーターのロード処理を、以下のように準備する。
vec_dim = 400
epochs = 100
batch_size = 200
input_dim = len(words)+1
#unk_dim = len(words_unk)+1
output_dim = input_dim
n_hidden = int(vec_dim*1.5) # 隠れ層の次元
# 単語予測用
prediction_words = Prediction(maxlen, n_hidden, input_dim, vec_dim, output_dim)
model_words = prediction_words.create_model()
# パラメーターロード
print()
print('単語分類用ニューラルネットパラメーターロード')
model_words.load_weights('param_1.hdf5')
print()
メイン処理は以下のとおりである。
n_init = 6000
# 単語
x_validation = x_train[n_init, :, :]
x_validation = x_validation.T
row = x_validation.shape[0] # 評価データ数
x_validation = x_validation.reshape(row, maxlen)
text_gen = '' # 生成テキスト
for i in range(0, maxlen) :
text_gen += indices_word[x_validation[0, i]]
print(text_gen)
print()
# 正解データ
text_correct = ''
for j in range(0, 4) :
x_correct = x_train[n_init+j*maxlen, :, :]
x_correct = x_correct.T
x_correct = x_correct.reshape(row, maxlen)
for i in range(0, maxlen) :
text_correct += indices_word[x_correct[0, i]]
print('正解')
print(text_correct)
print()
# 文生成
for k in range(0, 100) :
ret = model_words.predict(x_validation, batch_size=batch_size, verbose=0)
ret_word = ret.argmax(1)[0]
#print(indices_word[ret_word])
text_gen += indices_word[ret_word] # 生成文字を追加
x_validation[0, 0:maxlen-1] = x_validation[0, 1:maxlen]
x_validation[0, maxlen-1] = ret_word # 1文字シフト
print()
print(text_gen)
n_initは、入力となる文字列の基点を定義するインデックスである。何を指定してもよいが、固定にしておくと、ニューラルネット改善による差を比較しやすい。
リストの最後の方のループで、単語の予測と文書生成を実行している。予測単語のインデックスを最後尾に付けたリストを、次の単語予測の入力とする。
各リストを以下の順で実行すると、文章というか、文字列が生成される。
- リスト1
- リスト2
- リスト3 (ここまで共通)
- リスト4-2
- リスト5-1
- リスト7
- リスト8
文書生成の結果は以下のとおりである。
まず、「お題」となる入力文字列は以下のとおり。
「はございますまいか。考えて見れば、この世界の、人目につかぬ隅々では、どの様にUNK、恐ろしい事柄が、行われているか、ほんとうに想像の外《ほか》で」
これに対する生成文書は以下のとおり。
「はございますまいか。考えて見れば、この世界の、人目につかぬ隅々では、どの様にUNK、恐ろしい事柄が、行われているか、ほんとうに想像の外《ほか》で
はないか。それは、UNKのUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK《UNK》のUNK」
ちなみに正解は、以下のとおりである。
「はございますまいか。考えて見れば、この世界の、人目につかぬ隅々では、どの様にUNK、恐ろしい事柄が、行われているか、ほんとうに想像の外《ほか》でございます。無論始めの予定では、盗みの目的を果しさえすれば、すぐにもホテルを逃げ出す積《つも》りでいたのですが、世にも奇怪な喜びに、夢中になった私は、逃げ出すどころか、いつまでもいつまでも、椅子の中をUNKのUNKにして、その生活を続けていたのでございます。UNK《UNK》の外出には、注意に注意を加えて、少しも物音を立てず、又人目に触れない様にしていましたので、当然、危険はありませんでしたが、それにしても、数ヶ月という、長い」
Copyright© Digital Advantage Corp. All Rights Reserved.