ディープラーニング習得、次の一歩

ディープラーニング習得、次の一歩

ディープラーニングで自動筆記 - Kerasを用いた文書生成(前編)

2018年7月5日

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

石垣哲郎 石垣 哲郎

ディープラーニングによる文書生成

 AIによって仕事が奪われるとか、いわゆるシンギュラリティであるとか、AIの能力が人類を凌駕(りょうが)するなどと巷間(こうかん)に喧伝(けんでん)される今日このごろである。そんな中で、「ものを書くAI」というのも話題に上ることが多く、企業業績サマリー記事の自動生成や、あるいはAIが「執筆」した文書の著作権はどこに帰属するか、という話も聞こえてくるようになってきている。

 では、AIは本当に文章を書けるようになるのだろうか。これについては、筆者は懐疑的である。文章というのは思考の外界への投影であるが、少なくともディープラーニングベースのAIは、何か考えているわけではないからである。

 ディープラーニングの本質は、n次元ベクトル空間の点集合を超平面で仕切ってみせることである。ディープラーニングにおける学習とは、一番よく仕切れる超平面を決定することで、これは損失関数の最小値問題に還元される。「ディープ」というからには何か人知の及ばぬ深いことをやっているのでは、という感じがするが、実際は超平面の仕切りを多段にやっているだけのことで、入力と出力以外に「隠された」(いや、別に隠しているつもりはないが)多段の仕切り(いわゆる隠れ層)があるからディープなんだとのことである。

 このように、ディープラーニングとは線形代数(超平面)と偏微分(最小値問題)から構成される数学的プロセスであって、そこに思考が介在する余地はない。ディープラーニングは画像認識の分野で圧倒的な成功を収めているが、これは要するに、猫かどうか判別するのに脳みそは要らないということが、明らかになったということである。

 ディープラーニングにおける自然言語処理も同様に、線形代数と偏微分の集まりである。文字列を何らかの方法でn次元ベクトル空間に埋め込んで、超平面で仕切って分類する。分類数を語彙(ごい)数と等しくすれば、次の文字を予測できることになるが、それは決して思考した結果ではない。どんなに予測精度が上がっても、それは結局、猿真似の域を出ない。

 「そうはいっても、AIがチューリングテストを突破しかけている」という話が聞こえてくる昨今である。「猿真似もここまでくれば上等!」ということで、今回はKerasを用いて文書生成にチャレンジする。なお、おいおい明らかになることであるが、猿真似の域に到達するのは決して容易ではない。大抵の試行は猿のレベルにすらたどり着けなかった。低レベルの例えとして猿を引き合いに出したことは、猿に対して大変失礼であったと、反省することしきりである。

なぜ文書生成か

 ディープラーニングによる自然言語処理というと、まず「seq2seq(sequence to sequence)」という手法が思い浮かぶ。これは名称のとおり、入力の文字列から、出力として文字列を得るもので、翻訳分野で大きな成功を収めている。

 しかし、seq2seqは実装が難しい。「seq2seqレイヤー」とかがあって、それを呼べば一件落着、などというわけにはいかなかった(よく探せば、seq2seq用ライブラリがGitHubに登録されていたりするが、筆者は気が付かなかった)。

 そこで文書生成である。筆者は以下のリンク先を参考にしたが、基本的な考え方は、入力の文字列からLSTM(Long Short-Term Memory)を用いてまず次の1文字を予測し、これを入力文字列の最後尾に付けることで、逐次的に文字を予測して文書を生成する、というものである。図1にそのイメージを示す。

図1 文書生成の基本的な考え方

 仕組みが分かりやすく単純であり、実装も容易そうなので、筆者はすぐに成果が得られると期待した。実際は苦労の連続だったのだが、今回は前後2回に分けて、その経緯と顛末(てんまつ)を記述していくこととする。

本稿のゴール

 文書生成のニューラルネットワーク(以下、ニューラルネット)を実際に構築して、文書を生成してみる。文書生成に当たっては、1文字単位で予測する方法もあるが、今回は単語を予測する。入力文字列を分かち書きする必要があるが、これは形態素解析を用いて行う。

 本稿ではニューラルネットにLSTM(Long Short-Term Memory)を採用し、Keras(バックエンド:TensorFlow)を使って実装する。また、TensorFlowやKerasはインストール済みを前提に論を進める。

 なお、データ量が多いため、本稿に記載するコードを実行する際には、GPUマシンか、クラウドのGPUサービス利用を推奨する。

訓練データの入手

 三度(みたび)、江戸川乱歩先生にお出ましをいただく。すなわち、前回のword2vecの記事で使用した、江戸川乱歩の著作から作成したコーパスを、そのまま利用することとする。元テキストは青空文庫で入手可能である。

形態素解析

 形態素解析とは、簡単に言うと、日本語の文章を単語に分解することである。これも前回記事同様、京大黒橋・河原研究室のJUMAN++を使用する。

 以下にJUMAN++のURLを記す。ここからソフトウェアをダウンロードし、PCにインストールして対象文章を入力すると、分かち書きされた出力がカンマ区切りで得られるので、これをCSVファイルとして保存しておく。

 前回記事でJUMAN++を用いて作成したCSVファイルを、そのまま使用する。

実装

 いよいよ実装である。参考情報として、筆者が実行したソースコードを以下に示す。これらをJupyter Notebook上で、コードブロック単位に実行した。使用した各種ソフトのバージョンは、以下のとおりである。

  • Python: 3.6.4
  • anaconda: 5.1.0
  • tensorflow-gpu: 1.3.0
  • Keras: 2.1.4
  • jupyter: 4.4.0

 まず、各種import宣言である。

Python
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import csv
import pandas as pd
import random
import numpy.random as nr
import sys
import h5py
import keras
import math

from __future__ import print_function
from keras.layers.core import Activation
from keras.layers.core import Dense
from keras.layers.core import Dropout
from keras.layers.core import Flatten
from keras.layers.core import Masking
from keras.models import Sequential
from keras.layers import Input
from keras.models import Model
from keras.layers.recurrent import SimpleRNN
from keras.layers.recurrent import LSTM
from keras.layers.embeddings import Embedding
from keras.callbacks import EarlyStopping
from keras.callbacks import ReduceLROnPlateau
from keras.layers.normalization import BatchNormalization
from keras.initializers import glorot_uniform
from keras.initializers import uniform
from keras.initializers import orthogonal
from keras.initializers import TruncatedNormal
from keras.optimizers import RMSprop
from keras import regularizers
from keras.constraints import maxnorm, non_neg
from keras.utils.data_utils import get_file
from keras.utils import np_utils
リスト1 import宣言

 次に、CSVファイルの読み込み処理であるが、これは前回のword2vecの記事と同じである。

Python
# 元データ
df1 = csv.reader(open('rampo_separate.csv', 'r'))
df2 = csv.reader(open('rampo_separate2.csv', 'r'))
df3 = csv.reader(open('rampo_separate3.csv', 'r'))


data1 = [ v for v in df1]
data2 = [ v for v in df2]
data3 = [ v for v in df3]

mat1 = np.array(data1)
mat2 = np.array(data2)
mat3 = np.array(data3)

mat = np.r_[mat1[:, 0], mat2[:, 0], mat3[:, 0]]
print(mat.shape)
リスト2 CSVファイル読み込み

辞書データの作成

 出現する単語に一意のインデックス番号を付与し、インデックス←→単語文字列の両引きができる辞書を作成する。これも、word2vecのときと同じである。

 単語数が多いとニューラルネットの出力次元数が大きくなり、学習コストが高まる。そこで、出現頻度が低い単語(以下の事例では3回以下 )は思い切って割り切り、十把一絡げ(じっぱひとからげ)に「UNK」文字列(未知語)に置き換える。これで、本稿の事例では出力次元数が1/3以下になった。

Python
words = sorted(list(set(mat)))
cnt = np.zeros(len(words))

print('total words:', len(words))
word_indices = dict((w, i) for i, w in enumerate(words))  # 単語をキーにインデックス検索
indices_word = dict((i, w) for i, w in enumerate(words))  # インデックスをキーに単語を検索

# 単語の出現数をカウント
for j in range(0, len(mat)):
  cnt[word_indices[mat[j]]] += 1

# 出現頻度の少ない単語を「UNK」で置き換え
words_unk = []                # 未知語一覧

for k in range(0, len(words)):
  if cnt[k]<=3 :
    words_unk.append(words[k])
    words[k] = 'UNK'

print('低頻度語数:', len(words_unk))           # words_unkはUNKに変換された単語のリスト

words = sorted(list(set(words)))
print('total words:', len(words))
word_indices = dict((w, i) for i, w in enumerate(words))  # 単語をキーにインデックス検索
indices_word = dict((i, w) for i, w in enumerate(words))  # インデックスをキーに単語を検索
リスト3 辞書データ作成

訓練データ作成

 元データは文字列のリストなので、これを学習データにするには、先に作った辞書を使って数字に置き換える必要がある。置き換えの際に、低頻度単語には「UNK」文字列のインデックスを使用する。

 作成した配列(コード上の「mat_urtext」)から訓練データとラベルデータを作成する。作成イメージを図2に示す。

図2 訓練データ、ラベルデータの作成イメージ

 コード中の「maxlen」は入力系列数、すなわち入力文字数である。値を40にしてあるが、これは参考にしたコードの値を踏襲した。

Python
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 = np.array(x).reshape(len(data), maxlen, 1)
t = np.array(t).reshape(len(data), 1)
print(x.shape, t.shape)

n_train = int(len(data)*0.9)                     # 訓練データ長
x_train, x_validation = np.vsplit(x, [n_train])  # 元データを訓練用と評価用に分割
t_train, t_validation = np.vsplit(t, [n_train])  # targetを訓練用と評価用に分割
リスト4 訓練データ作成

 ソースコード中で、「# 0パディング対策」というコメントが付いた行がある。実はKerasには、入力系列数が可変であっても、欠損データとして0を指定すれば、その入力を無視してLSTMが入力系列全体を処理できる機能がある。つまり入力系列に0を使うわけにはいかないので、インデックス0のデータに別のインデックスを付与する。

 この処理により、入力次元数は単語数+1となる。

 データは9:1に分割し、長い方にはx_traint_trainという名称を付けて、訓練に使用する。一方、短い方にはx_validationt_validationという名前を付けて、学習後のテストデータとする。

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

 ニューラルネットはKerasのSequentialモデルを用いて構築する。

 学習データは、各単語のインデックス、すなわち整数の列になっている。これをOne-hotベクトル(ある1つの次元だけが1で、その他が0のベクトル。今回の例では、インデックスの数字に相当する次元を1にする)の列に変換したものが、ニューラルネットの入力になる。

 処理の流れは、

1入力のOne-hotベクトルを実数ベクトル空間に埋め込む(Embeddingレイヤー)
2バッチ正規化(BatchNormalizationレイヤー)
3入力の欠損用に0を予約する(Maskingレイヤー)
4LSTMレイヤー
5バッチ正規化(BatchNormalizationレイヤー)
6ドロップアウトレイヤー
7出力次元へ変換(Denseレイヤー)
8活性化(Activationレイヤー)

となる。

 バッチ正規化というのは、平均0、分散1になるようにパラメーターを変換する処理で、これをやると学習が速く進むという触れ込みであるが、確かに目に見えて効果がある。

 create_modelメソッドでインスタンスを生成し、trainメソッドで学習を実行する。学習の発散を防ぐため、trainメソッド内でEarly-Stopping(=学習を適切なタイミングで早期に打ち切る機能)を使用する。

 各パラメーターの初期値は乱数で与えられるが、これが試行のたびに毎回同じになるように、乱数のseed指定を行う。

 trainメソッド内のfitのパラメーターvalidation_split0.1が指定されているが、これは入力データの10%を評価用に使用するという意味である。

Python
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ベクトルに変換してくれるという、優れものである。

 なお、学習と文書生成を別のタイミングで実施できるように、学習用のニューラルネットと、文書生成用のニューラルネットは分けることにした。文書生成用ニューラルネットで使えるように、学習済みのパラメーターをセーブしてある。

Python
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割に満たなかった。そこで汎化性能はとりあえずあきらめて、全データを学習用に投入することにした。

 そのためのコードを以下に示す。

 まず、訓練データ作成である。データを訓練用とテスト用に分けるのをやめている。

Python
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になっているところである。

Python
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 ニューラルネットワーク本体(全データ学習)

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

Python
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を、以下に入れ替える。訓練データ作成処理とほとんど同じだが、本処理は文書生成の初期値を入手するためのものであり、訓練に使用するわけではないので、シャッフルの処理がなくなっている。

Python
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 文書生成用データ作成

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

Python
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 文書生成用ニューラルネット活性化とパラメーターロード

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

Python
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》の外出には、注意に注意を加えて、少しも物音を立てず、又人目に触れない様にしていましたので、当然、危険はありませんでしたが、それにしても、数ヶ月という、長い」

単語出現頻度による分類と、正解率改善

 学習結果の改善をいろいろ試行錯誤するうちに、出現頻度の近い単語だけを集めて学習すると、精度が改善することに気が付いた。そこで、最初に出現頻度を予測して単語を頻度別に分類し、ついで単語自体を予測するという、2段構えのニューラルネットを考案した。

 単語を出現頻度に応じて、以下の7種類に分類する。

  • 3~10
  • 10~28
  • 28~100
  • 100~300
  • 300~2000
  • 2000~15000
  • 15000以上

 分類の数や境界値は、いろいろ試行錯誤して決定したもので、この値でなければならないという強い理由があるわけではない。

 全体構成のイメージを図3に示す。

図3 単語出現頻度による分類を取り入れたニューラルネット構成

出現頻度による単語分類

 分類のために、新たにラベルデータを準備する必要がある。以下はそのコードである。

Python
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, :])
  # 出現頻度に応じてラベルの値を設定
  if cnt[mat_urtext[i+maxlen, :]] < 10 :       # 頻度が10未満なら0
    target.append(0)
  elif cnt[mat_urtext[i+maxlen, :]] < 28 :     # 頻度が28未満なら1
    target.append(1)
  elif cnt[mat_urtext[i+maxlen, :]] < 100 :    # 頻度が100未満なら2
    target.append(2)
  elif cnt[mat_urtext[i+maxlen, :]] < 300 :    # 頻度が300未満なら3
    target.append(3)
  elif cnt[mat_urtext[i+maxlen, :]] < 2000 :   # 頻度が2000未満なら4
    target.append(4)
  elif cnt[mat_urtext[i+maxlen, :]] < 15000 :  # 頻度が15000未満なら5
    target.append(5)    
  else  :                                     # 頻度が15000以上なら6
    target.append(6)     
    
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 = np.array(x).reshape(len(data), maxlen, 1)
t = np.array(t).reshape(len(data), 1)
print(x.shape, t.shape)

x_train = x         # 元データを訓練用と評価用に分割しない
t_train = t

print(t_train.max(0))
リスト4-3 分類用訓練データ作成

 ラベルデータtargetの値に、予測対象単語の出現頻度に応じて、0から6までのいずれかを設定している。

 ニューラルネットの定義には変更はない。メイン処理は、ラベルデータの次元が変わる関係上、変更が入る。

Python
vec_dim = 400 
epochs = 100
batch_size = 200
input_dim = len(words)+1
output_dim = 7

#t_dim = len(over_20)+1
n_hidden = int(vec_dim*1.5)  # 隠れ層の次元

prediction = Prediction(maxlen, n_hidden, input_dim, vec_dim, output_dim)

emb_param = 'param2_1_classify_by_7.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)            # 学習済みパラメーターセーブ
リスト6-2 分類用メイン処理

 変更点は、ニューラルネットのインスタンス生成や学習時に指定する、ラベルデータの次元である。

 実行コードと実行順を整理すると、以下のとおりである。

  • リスト1
  • リスト2
  • リスト3 (ここまで共通)
  • リスト4-3
  • リスト5-1
  • リスト6-2

 正解率は47%程度になった。

出現頻度別単語予測

 次に単語予測である。単語の出現頻度ごとに学習を実施する。訓練データ作成用のコードは以下のとおり。

Python
maxlen = 40      # 入力語数
n_upper = 10       # 学習対象単語の出現頻度上限
n_lower = 0        # 学習対象単語の出現頻度下限

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):
  # 答えの単語の出現頻度がn_lower以上でかつn_upper 未満の場合を学習対象にする
  if cnt[mat_urtext[i+maxlen, :]]>=n_lower and cnt[mat_urtext[i+maxlen, :]] < n_upper:
    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)

z = list(zip(x_train, t_train))
nr.seed(12345)
nr.shuffle(z)                 # シャッフル
x_train, t_train = zip(*z)

x = np.array(x_train).reshape(len(data), maxlen, 1)
t = np.array(t_train).reshape(len(data), 1)


x_train = x
t_train = t

print(x_train.shape, t_train.shape)
リスト4-4 単語予測用訓練データ作成

 学習対象範囲を、n_upperおよびn_lowerによって指定する。

 ニューラルネットの定義(リスト5-1)とメイン処理(リスト6-1)は変更ないが、メイン処理ではパラメーターファイル名が被らないよう、名称定義に注意すること。

 以下、各パラメーターファイルは以下の名称が付与されている前提で、論を進める。

頻度区分パラメーターフェイル名
3~10param_words_0_0_10.hdf5
10~28param_words_1_10_28.hdf5
28~100param_words_2_28_100.hdf5
100~300param_words_3_100_300.hdf5
300~2000param_words_4_300_2000.hdf5
2000~15000param_words_5_2000_15000.hdf5
15000以上param_words_6_15000_400000.hdf5
表1 パラメーターファイル名

 コードの実行順は以下のとおり。

  • リスト1
  • リスト2
  • リスト3 (ここまで共通)
  • リスト4-4
  • リスト5-1
  • リスト6-1

 つまり、リスト4-4とリスト5-1を実行した後、リスト6-1のemb_paramparam_words_0_0_10.hdf5に変えて実行する。さらに、リスト4-4のn_upper010に、n_lower028に変えて、リスト6-1のemb_paramparam_words_0_0_10.hdf5param_words_1_10_28.hdf5に変えて再実行する。この要領で、表1のすべての頻度区分を順に実行していく。

 この対策により、単語予測の正解率は9割以上に改善した。単語分類と合わせた全体の正解率は、36%以上という計算になる。

結果確認

 正解率改善の結果が文書生成にどれくらい反映されているか、確認する。

 ニューラルネットの活性化と、学習済みパラメーターのロード処理は、以下のとおりである。都合8つのニューラルネットを生成し、おのおのに訓練したパラメーターを設定する。

Python
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)  # 隠れ層の次元

# 頻度分類用
print('頻度分類用ニューラルネット活性化')
prediction_freq = Prediction(maxlen, n_hidden, input_dim, vec_dim, 7)
model_classify_freq_0 = prediction_freq.create_model()
print()
# 単語予測用
prediction_words = Prediction(maxlen, n_hidden, input_dim, vec_dim, output_dim)
print('単語分類用ニューラルネット(0_10)活性化')
model_words_0_10 = prediction_words.create_model()
print('単語分類用ニューラルネット(10-28)活性化')
model_words_10_28 = prediction_words.create_model()
print('単語分類用ニューラルネット(28-100)活性化')
model_words_28_100 = prediction_words.create_model()
print('単語分類用ニューラルネット(100-300)活性化')
model_words_100_300 = prediction_words.create_model()
print('単語分類用ニューラルネット(300-2000)活性化')
model_words_300_2000 = prediction_words.create_model()
print('単語分類用ニューラルネット(2000-15000)活性化')
model_words_2000_15000 = prediction_words.create_model()
print('単語分類用ニューラルネット(15000-400000)活性化')
model_words_15000_400000 = prediction_words.create_model()
print()

# パラメーターロード
print('頻度分類用ニューラルネットパラメーターロード')
model_classify_freq_0.load_weights('param2_1_classify_by_7.hdf5')
print()
print('単語分類用ニューラルネット(0-10)パラメーターロード')
model_words_0_10.load_weights('param_words_0_0_10.hdf5')
print('単語分類用ニューラルネット(10-28)パラメーターロード')
model_words_10_28.load_weights('param_words_1_10_28.hdf5')
print('単語分類用ニューラルネット(28-100)パラメーターロード')
model_words_28_100.load_weights('param_words_2_28_100.hdf5')
print('単語分類用ニューラルネット(100-300)パラメーターロード')
model_words_100_300.load_weights('param_words_3_100_300.hdf5')
print('単語分類用ニューラルネット(300-2000)パラメーターロード')
model_words_300_2000.load_weights('param_words_4_300_2000.hdf5')
print('単語分類用ニューラルネット(2000-15000)パラメーターロード')
model_words_2000_15000.load_weights('param_words_5_2000_15000.hdf5')
print('単語分類用ニューラルネット(15000-400000)パラメーターロード')
model_words_15000_400000.load_weights('param_words_6_15000_400000.hdf5')
print()
リスト7-1 文書生成用ニューラルネット活性化とパラメーターロード(出現頻度による単語分類)

 文書生成のメイン処理は以下のとおりである。予測した単語頻度ごとに、専用のニューラルネットを用いて単語を予測する。

Python
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_0  = model_classify_freq_0.predict(x_validation, batch_size=batch_size, verbose=0)  # 評価結果
  ret_0 = ret_0.reshape(row, 7)
  flag_0 = ret_0.argmax(1)[0] 
  if flag_0 == 0 :
    ret = model_words_0_10.predict(x_validation, batch_size=batch_size, verbose=0)
    ret_word = ret.argmax(1)[0] 
  
  elif flag_0==1 :
    ret = model_words_10_28.predict(x_validation, batch_size=batch_size, verbose=0)
  elif flag_0==2 :
    ret = model_words_28_100.predict(x_validation, batch_size=batch_size, verbose=0)
  elif flag_0==3 :
    ret = model_words_100_300.predict(x_validation, batch_size=batch_size, verbose=0)
  elif flag_0==4 :
    ret = model_words_300_2000.predict(x_validation, batch_size=batch_size, verbose=0)
  elif flag_0==5 :
    ret = model_words_2000_15000.predict(x_validation, batch_size=batch_size, verbose=0)
  else :
    ret = model_words_15000_400000.predict(x_validation, batch_size=batch_size, verbose=0)

  ret_word = ret.argmax(1)[0] 

  #print(flag_0, 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-1 文書生成処理(出現頻度による単語分類)

 コードの実行順は以下のとおり。

  • リスト1
  • リスト2
  • リスト3 (ここまで共通)
  • リスト4-2
  • リスト5-1
  • リスト7-1
  • リスト8-1

 実行結果は以下のとおりである。

「はございますまいか。考えて見れば、この世界の、人目につかぬ隅々では、どの様にUNK、恐ろしい事柄が、行われているか、ほんとうに想像の外《ほか》で
はありません。あの出来事は、僕がすっかり抜いて置いたんだな。これはどうしてまあ、何という恐ろしい事だ。そこで、僕は随分可能性をハッキリとしたのです。これは自分の訊問に対しても、余りにUNKことがあるのです。彼は思わず、ハッとした様に、あらゆる事情をUNK、最も深く考えていた。彼は、そうして、旅をして、何という事の次第を取出して、恐る恐るの穴に光っている。そして」

 だいぶ日本語らしくなった。「てにをは」も割としっかりしている。しかし、脈絡のなさにはちょっと笑ってしまう。

次回予告

 さらなる正解率改善施策により、どこまで自然な文章にできるか、チャレンジする。

 最初はもっと簡単に文書生成ができると予想していたが、思いの外、苦戦した。正解率がなかなか向上しないのが意外であったが、いくつか施策を準備しているので、次回はその結果を報告する。

  • このエントリーをはてなブックマークに追加
ディープラーニング習得、次の一歩
1. Kerasを用いたディープラーニング(LSTM)による株価予測

ディープラーニングのチュートリアルが一通り終わったら、次に何をやる? 今回は、誰にでも簡単にできる「株価予測」をテーマに、LSTMのニューラルネットワークを、Kerasを使って実装する方法を説明する。

2017年12月11日(月)
ディープラーニング習得、次の一歩
2. 挑戦! word2vecで自然言語処理(Keras+TensorFlow使用)

自然言語のベクトル化手法の一つである「word2vec」を使って、単語間の関連性を表現してみよう。Keras(+TensorFlow)を使って実装する。

2018年1月30日(火)
ディープラーニング習得、次の一歩
3. word2vecリターンズ! 品詞分類による精度改善

Keras(+TensorFlow)を使って自然言語のベクトル化手法「word2vec」を実装。学習データに品詞分類を追加することによって、前回よりも予測精度が改善するかを検証する。

2018年4月5日(木)
ディープラーニング習得、次の一歩
4. 【現在、表示中】≫ ディープラーニングで自動筆記 - Kerasを用いた文書生成(前編)

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

2018年7月5日(木)
ディープラーニング習得、次の一歩
5. ディープラーニングで自動筆記 - Kerasを用いた文書生成(後編)

「文書生成」チャレンジの後編。ネットワークにLSTM、ライブラリにKeras+TensorFlowを採用し、さらに精度を改善していく。最後に、全然関係ない入力文章から、江戸川乱歩風文書が生成されるかを試す。

2018年8月8日(水)
Deep Insider の SNS :