取組の概要については、機械学習(AI)による単語予測と、法令文を校正するための予測精度の向上についての取組(概要)を参照されたい。
試行の説明については、機械学習(AI)による単語予測と、法令文を校正するための予測精度の向上についての取組(技術的説明:試行編)を参照されたい。
##まえがき
この取組でのほとんどの作業はGoogle Colab上で行うことで、データを残すように心がけた。それらのデータはgithubのリポジトリであるchuukai/proofread-jlawにコピーを置いている。
ただし、XMLファイルからSentenceタグの内容を抜き出す作業や、拗音及び促音に用いる「や・ゆ・よ・つ」の表記を小書きに変換する作業、ファイルの一覧を作成する作業等は、Perlなどを使ったコマンドラインでの処理が簡便であったため、データが残っていない場合がある。
##モデル作成の前処理
より適切な単語を候補としてあげられるようなモデルを作成するため、以下のような処理を行った。
###法令データに対する変換及び削除処理
法令データは、XML一括ダウンロードのページのメニューで「法令分類データ」を選択し、分野別でダウンロードした。
本取組では2021/5/10時点の法令データを使用した。
####分かち書きできない文章をできるだけ削除する
1.カタカナ文語体の法令を学習対象から除く
昭和21年以前に制定された法令はカタカナの文語体で記述されており、ひらがな口語体を前提とした分かち書きツールで分かち書きできない。他のツールでカタカナをひらがなに一括変換することは比較的に容易だが、文語体を口語体に一括変換するツールが思い当たらず、また、変換できたとしてもそのコストが昭和21年以前に制定された法令を学習対象に入れるメリットを上回らないと見込まれたため、昭和21年以前に制定された法令を一括して学習対象から除いた。具体的には、ダウンロードしたxmlファイルのうち、ファイル名が1(明治)、2(大正)、301(昭和元年)〜321(昭和21年)から始まるファイルを学習に使わなかった。
これにより、廃止されていない有効な法令の一部(例えば、現在でもカタカナ文語体で改正して運用されている鉄道営業法など)が学習対象から除かれた。
また、昭和22年以後に制定された法令の一部において、昭和21年以前の法令のカタカナ文語体を読み替え等で引用している場合があるが、当該引用部分についてひらがな口語体への変換等をしていない。学習にあたっては、当該引用部分について、ひらがな口語体を前提とする分かち書きツールで分かち書きしているため、学習結果にノイズとして残った可能性がある。
2.目次やタイトルなどの記載を削除する
法令データはXMLで記述されており、文章はSentenceを要素とするタグで囲まれている。このため、Sentenceを要素とするタグの内容部分を抜き出して使用し、それ以外の目次やタイトルなどの記載を使わないこととした。
nkf -w ./河川/202AC0000000016_20150801_000000000000000/202AC0000000016_20150801_000000000000000.xml | xml_grep 'Sentence' --text_only /dev/stdin > ./河川/202AC0000000016_20150801_000000000000000.sentence
#このシェルスクリプトは以下、同様のコマンドラインが続く。
#nkfはxmlファイルの文字コードをシステムが使用している文字コードに変換するために使用している。
#xml_grepはperlのコマンドであり、cpanからインストールしている。
3.拗音及び促音である大ぶりのひらがなを小書きのひらがなに変換する
法令における拗音及び促音に用いる「や・ゆ・よ・つ」の表記については、昭和64年から小書きとなった。すなわち、昭和22年から昭和63年までの法令は、拗音及び促音に用いる「や・ゆ・よ・つ」はおおぶりであった。
おおぶりのまま分かち書きツールにかけたところ、おおぶりの「つ」とその前後の文字があわせて名詞として分かち書きされる箇所を複数発見した。このため、昭和22年から昭和63年までの法令の「ゆ・よ・つ」の表記を、小書きに変換して学習対象にしたほうが予測精度が上がると見込まれるという観点から、小書きに変換した。なお、拗音の「や」は全く無いかほとんど見られず、誤変換の恐れがあったため、変換対象にしなかった。
cat - | perl -p -Mutf8 -CSD -e 's/ヽ//g;' -e 's/([あ-わが-どば-ぼぱ-ぽ])つ[たて]/$1っ$2/g;' -e 's/(\p{Han})つ([たて])/$1っ$2/g;'
#その他、's/([き|ぎ|し|じ|ち|ぢ|に|ひ|び|ぴ|み|り])よう/\1ょう/'や's/([き|ぎ|し|じ|ち|ぢ|に|ひ|び|ぴ|み|り])ゆう/\1ゅう/'という変換を行ったようであるが、シェルスクリプトやコマンド履歴を散逸した。
cat 322AC0000000003_20190430_429AC0000000063.sentence | sh normalize.sh > 322AC0000000003_20190430_429AC0000000063.sentence.nml
#このシェルスクリプトは以下、同様のコマンドラインが続く。
4.分かち書きツール(MeCab)と辞書(mecab-ipadic-NEologd)を使って分かち書きする
分かち書きツールには最も有名なMeCabのほかにも公表されているツールがあるが、検証のしやすさとネットでの説明の多さから、MeCabを使った。
辞書はmecab-ipadic-NEologdを使った。採用した理由は、最近の法令の名称が固有名詞として登録されていることである。他の辞書では、最近の法令の名称を一つの単語ではなく分かち書きすることがあった。しかし、法令の体系において法令の名称は固有名詞であり、使われた法令の名称は、使っている法令の上位や下位の規範であることがあって、法令の位置づけにかかわる重要な特徴である。また、候補単語に法令の名称を入れることは必須に近い重要な要件であった。
path = "-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd -b 819200"
m = MeCab.Tagger(path)
noun_list = ''
for bunrui in range(1,51):
lssentence = " ".join(['ls','/content/drive/My\ Drive/Colab\ Notebooks/houreibunrui20210510/%02d/*.nml' % bunrui])
output = subprocess.getoutput(lssentence)
tolists = output.split('\n')
for tolist in tolists:
print(tolist)
text_data = open(tolist, "rb").read()
decoded_text = text_data.decode('utf-8')
text = re.sub('\n\n', '\n', decoded_text)
with open(tolist + '.wwakati', 'a') as f:
parses = m.parse(text)
#以下略
####学習に使用する単語を品詞レベル及び文字列レベルで限定する
限られたメモリの中では、一部の品詞や文字列を校正の対象から外すことで学習の範囲を絞った方が、他の品詞での予測精度が上がるという見込みを持ったことから、品詞と文字列を指定して単語を削除した。これにより、学習に使用するデータセットのサイズを減らすことができた。
削除する品詞のリストはpythonのリストとしてハードコードしている。
if t_lists[0] == 'EOS' or t_lists[0] == '' or t_lists[0] == 'と' or t_lists[0] == 'の':
term = ''
continue
for hinsi in ['記号', '助詞,連体化', '助詞,副詞化', '助詞,係助詞', '助詞,接続助詞', '助詞,並立助詞', '助詞,副助詞/並立助詞/終助詞', '助詞,格助詞,引用', '助詞,格助詞,一般,*,*,*,へ,ヘ,エ', '助詞,格助詞,一般,*,*,*,に,ニ,ニ', '助詞,格助詞,一般,*,*,*,を,ヲ,ヲ', '助詞,格助詞,一般,*,*,*,で,デ,デ', '助詞,格助詞,一般,*,*,*,が,ガ,ガ', '助詞,格助詞,一般,*,*,*,から,カラ,カラ', '助詞,副助詞,*,*,*,*,まで,マデ,マデ', '連体詞', '接頭詞,名詞接続', '特殊', '形容詞,自立,*,*,形容詞・アウオ段,基本形,ない,ナイ,ナイ', '接頭詞,数接続', '名詞,数', '名詞,接尾', '名詞,非自立,副詞可能', '名詞,非自立,助動詞語幹', '名詞,接尾,助数詞', '名詞,代名詞', '助動詞', '名詞,非自立,一般', '動詞,非自立', '動詞,自立,*,*,サ変・スル', '動詞,接尾', '動詞,自立,*,*,一段,基本形,できる,デキル,デキル', '動詞,自立,*,*,五段・ラ行,基本形,係る,カカル,カカル', '助詞,格助詞,連語,*,*,*,に']:
if t_lists[1].find(hinsi) > -1:
term = ''
for stopword in ['ヶ所', 'ヵ所', 'カ所', '箇所', 'ヶ月', 'ヵ月', 'カ月', '箇月', '十', '百', '千', '万', '億', '兆', 'もの', '明治', '大正', '昭和', '平成', '令和']:
if t_lists[0].find(stopword) > -1:
term = ''
####同一法令で改正後(未施行)のデータがある場合は改正後の法令を削除する
法令ファイルには改正前と改正後(未施行)が重複している場合があるため、ファイル名の前半部分を使って改正後の法令を対象から落として、重複を取り除いた。これにより、ほぼ同じ内容の法令を複数回学習することを防いだ。
!find /content/drive/My\ Drive/Colab\ Notebooks/houreibunrui20210510/ -name "*.wwakati" -type f -printf "%p,%s\n" > /content/drive/My\ Drive/Colab\ Notebooks/houreibunrui20210510/bunruiallw.txt
#このコマンドにより、以下のような内容のテキストファイルが出力される。
#/content/drive/MyDrive/Colab Notebooks/houreibunrui20210510/01/322AC0000000003_20190430_429AC0000000063.sentence.nml.wwakati,5724
#(以下略)
#01は分類番号、322AC0000000003_20190430_429AC0000000063.sentence.nml.wwakatiは分かち書き後のファイル名、5724はファイルサイズ(byte単位)である。
bunruiallw.txtをダウンロードし、vimとsortとuniqを使って編集した。
編集の手順としては、/content/drive/MyDrive/Colab Notebooks/houreibunrui20210510/01/322AC0000000003_20190430_429AC0000000063.sentence.nml.wwakati,5724
を322AC0000000003,20190430,429AC0000000063.sentence.nml.wwakati/01/5724
となるように正規表現を使ってvimで置換し、bunruiwall-sort-uniq2.csvという名前で保存した後、sortコマンドとuniqコマンドを使って、改正がない法令と改正前の法令のファイル名を残した。
###分かち書き後の法令文を結合する
分かち書き後の法令文のすべてを結合してデータセットとした場合、学習時にはメモリ不足となり、また、あまりに大きなデータセットは十分なパフォーマンスを得られないという一般的な事象が生じることが予想された。
そのため、法令文を結合した後のテキストファイルを、学習プロセス(model.fit)がメモリ不足のために落ちることがない一定の分量に制限した。本取組ではこの分量を2,160,000bytesとし、それでもプロセスが落ちた場合は1,690,000bytesとした。この数値はメモリ容量とテキストの内容により変化する。
結合する際には、まず法令を分野ごとに分け、法令のファイル名の昇順(≒年代順)で法令をテキストファイルに加えていき、テキストファイルの容量が一定の分量に至れば結合を止めて保存することを繰り返すことが考えられる。
しかし、単純にこの工程を繰り返すと、法令の分野の最後の方で法令が余り、一定の分量に満たない小さな結合済みファイルが作成される。このファイルから作られたモデルは、単語の予測精度が低い。こうなることを防ぐために、テキストファイルの容量がすべて一定の分量未満になるよう結合した。
ここで法令の分野でのファイルサイズ総量をsums、法令を結合した後のテキストファイルの個数をnums、結合後のテキストファイルのサイズをbfsiとする。
nums = sums // 1690000 + 1
bfsi = sums / nums + ((sums % -1690000) / nums - 1)) * -1) < 1690000
結合の手順は、ファイル名の昇順で法令をテキストファイルに加えていき、テキストファイルの容量が一定の分量に至れば結合を止めて保存することは同じだが、次の結合は法令の順番を遡ってから始める。遡りは、遡り合計ファイル容量sumbwがint(mods)/(int(nums) - 1)を超えないところまでとする。遡った先からテキストファイルの容量が一定の分量に至るまで、法令を結合し、.wwakatiallという拡張子を持つファイル名で保存した。
nums.append(sums[bunruii]//1690000 + 1)
mods.append(((sums[bunruii] % -1690000)//(nums[bunruii] - 1)) * -1)
sums.append(bfsi[2])
for t2, bfsbw in enumerate(reversed(bfs[start:start + t1])):
sumbw = sumbw + int(bfsbw[2])
if sumbw > int(mods[bunruii])/(int(nums[bunruii]) - 1):
sumfw = sumbw - int(bfsbw[2]) + int(bfsi[2])
wakatilist = wakatilistbw[:]
wakatilistbw = []
sumbw = 0
break
###結合した法令文からfastTextを使って単語分散表現を得る
次のようなコマンドにより、結合した法令文からfastTextを使って128次元に圧縮した単語分散表現を得た。
fasttext skipgram -dim 128 -minCount 1 -input 13-01w.wwakatiall -output 13-01w.wwakatiall.model
次元数を増やせば(例えば-dim 256にすれば)予測精度が向上したかもしれないが、メモリ不足対策のため、また、単語の種類が少ないため、次元数を128次元としている。
fno = fni + ".model"
fni = "/content/drive/My Drive/Colab Notebooks/houreibunrui20210510/01-50wakatiall/" + fni
fasttextcommand = ["/content/fastText-0.2.0/fasttext", "skipgram", "-dim", "128", "-minCount", "1", "-input", fni, "-output", fno]
###データセットを作成する
データセットの構造を以下のように定めた。
(文章中にある特定の単語の前方にある5個の単語,文章中にある特定の単語の索引番号,文章中にある特定の単語の後方にある5個の単語,文章中にある特定の単語)
1行にある単語ごとに上記のタプルを作成してリストとして連ねた。この処理を結合された法令の1行ずつに対して繰り返した。
with open(fnwakatiall, 'r') as f:
for fi, line in enumerate(f):
terms = line.split()
for cur in range(rng, len(terms) - rng, 1):
try:
head = list(map(lambda x:term_vec[x], terms[cur-rng:cur]))
ans = terms[cur]
tail = list(map(lambda x:term_vec[x], terms[cur+1:cur+rng]))
pure_text = terms[cur-rng:cur+rng]
dataset.append( (head, ansidx[ans], tail, pure_text) )
データセットには、ある単語と、その前方5個と後方5個の単語を入れている。その前後の単語は学習に使われる。本取組では単語の個数をrngという名称の変数として、5を代入している。この数値を増やせば、より多くの単語間の関係が学習に取り入れられるので、単語予測の精度は上がるであろう。しかし、数値を増やせばデータセットの容量は大きくなり、メモリ不足になるおそれがある。
本取組の初期にはrng=5として学習時にメモリ不足となっていた。しかし、メモリ不足対策としてrng=3とすると単語予測の精度が定性的に低くなる結果となった(結果のデータを残していないため、検証できない)。そのため、精度向上のためにrng=5となるように全体の初期値を見直した。そこで、学習に使用する単語から助詞等を除き、単語分散表現を128次元としたことにより、rngを5とすることができた。このような事情を鑑みれば、もしrngを5よりも増やすのであれば、全体を見直した現在以上のメモリ不足対策を講じなければならない。
##モデルを作成する
###モデルのネットワーク
活性化関数にsigmoid、損失関数にbinary cross entropyを用いたモデルである。
ネット上にあったモデルのネットワークをほぼそのまま利用させていただいている。1
違いは、入力の次元数を256から128にしている。
rng = 5
def build_model(maxlen=None, out_dim=None, in_dim=128):
print('Build model...')
model = Sequential()
model.add(GRU(128*rng*2, return_sequences=False, input_shape=(maxlen, in_dim)))
model.add(BN())
model.add(Dense(out_dim))
model.add(Activation('linear'))
model.add(Activation('sigmoid'))
optimizer = Adam()
model.compile(loss='binary_crossentropy', optimizer=optimizer)
return model
-
利用したのは「RNNで”てにをは”の蓋然性を計算します」というリポジトリにあるコードであり、ブログ『にほんごのれんしゅう』の「RNNで「てにをは」を校正する」という記事に説明がある。この記事がなければこの取組は成立しなかったため、大変感謝している。 ↩