検索
連載

ディープラーニングで自動筆記 − Kerasを用いた文書生成(前編)ディープラーニング習得、次の一歩(2/3 ページ)

ディープラーニングによる自然言語処理の一つ「文書生成」にチャレンジしてみよう。ネットワークにLSTM、ライブラリにKeras+TensorFlowを採用し、徐々に精度を改善していくステップを説明する。

Share
Tweet
LINE
Hatena

ニューラルネットワーク構築

 ニューラルネットは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_split0.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

リスト5 ニューラルネットワーク本体

メイン処理

 上記で定義したニューラルネットを動かして、学習を進める。入力次元数input_dimは単語数である。1を足しているのは、マスキング用に0を予約してあるからである。LSTMへの入力次元vec_dim400、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)

リスト6 メイン処理

 以上、ここまで、以下の順でコードを実行してきた。

  • リスト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)

リスト4-1 訓練データ作成(全データ学習)

 次にニューラルネット定義である。変わっているのは、validation_split0.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

リスト5-1 ニューラルネットワーク本体(全データ学習)

 最後にメイン処理である。スコア算出時に指定する学習データ、ラベルデータが変わっている。

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)

リスト6-1 メイン処理(全データ学習)

 ちなみに、コードの実行順をまとめると以下のとおりになる。

  • リスト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)

リスト4-2 文書生成用データ作成

 また、ニューラルネットの活性化と、学習済みパラメーターのロード処理を、以下のように準備する。

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()

リスト7 文書生成用ニューラルネット活性化とパラメーターロード

 メイン処理は以下のとおりである。

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)

リスト8 文書生成用メイン処理

 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.

[an error occurred while processing this directive]
ページトップに戻る