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

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

» 2018年07月05日 05時00分 公開
[石垣哲郎]
「ディープラーニング習得、次の一歩」のインデックス

連載目次

ご注意:本記事は、@IT/Deep Insider編集部(デジタルアドバンテージ社)が「deepinsider.jp」というサイトから、内容を改変することなく、そのまま「@IT」へと転載したものです。このため用字用語の統一ルールなどは@ITのそれとは一致しません。あらかじめご了承ください。

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

 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 文書生成の基本的な考え方 図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宣言である。

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の記事と同じである。

# 元データ
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以下になった。

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 訓練データ、ラベルデータの作成イメージ 図2 訓練データ、ラベルデータの作成イメージ

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

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という名前を付けて、学習後のテストデータとする。

       1|2|3 次のページへ

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。