アカデミック

【超初心者向け】KyTeaによるテキスト解析をpythonで実装してみる。

kyteaって…?
pythonで簡単な自然言語処理をしてみたい!

 

今回は,KyTeaという既成のツールキットを利用して簡単な言語処理を実装していきたいと思います。また,本記事はpython実践講座シリーズの内容になります。その他の記事は,こちらの「Python入門講座/実践講座まとめ」をご覧ください。

コーディングに関して未熟な部分がたくさんあると思いますので,もし何かお気づきの方は教えていただけると幸いです。また,誤りについてもご指摘していただけると非常に助かります。

問題設定

今回は,キャッチーさを重視して「暗号文解析」をしてみたいと思います。例えば,下のような暗号文をあるルールに則って解読していこうと思います。

Wewianoula.(①)

 

何じゃこれ??

 

今回の暗号文のルールは「英単語の頭から2文字を並べる」というものだとします。上の暗号文は,様々な解釈ができますが,一例としてこのような文章が想定できます。

We will analyze our languages.(②)

今回の目的は,①のような暗号文が与えられたときに,もっともらしい文章を生成するようなモデルを構築していきます。

 

kyTeaとは

KyTeaは「単語分割」「タグ付け」等の学習に特化した言語処理のツールキットです。こちらのページが本家のHPになります。基本的な使い方をまとめておきます。

以下はLinux環境もしくはMac OSX環境を想定しています。

 

インストール

本家のHPより最新版のKyTeaをダウンロードします。圧縮されていますので,以下のコマンドで解凍します。「X.X.X」にはダウンロードしたKyTeaのバージョンを入れてください。

tar -xzf kytea-X.X.X.tar.gz

 

続いて,解凍したファイルのディレクトリまで移動します。もちろん,自身のやりやすいディレクトリにKyTeaをコピーしてから行っても構いません。

cd kytea-X.X.X

 

その後,魔法のコマンドでソースコードをコンパイルします。

./configure
make
make install

 

もし,以下のコマンドを打ってエラーを吐かなければ,インストール成功です。

kytea --help

 

学習

学習のためには,コーパスが必要です。KyTeaに学習させるコーパスは,以下のような形式を取るようにします。

教師単語/アノテーション

 

今回は,単語の頭文字をアノテーションとして学習させていきます。

The/Th apple/ap is/is red/re.

 

このときの注意点として,冠詞の「a」に代表されるような1文字からなる英単語の場合は,ダミーとして「@」を付け加えることによって,全ての英単語を2文字からなる前提で話を進められるようにします。

A@/A@ teacher/te plays/pl baseball/ba.

 

これらのテキストファイルを利用して,Kyteaでは以下のコマンドによってモデルを学習させることができます。

train-kytea -full テキストファイルパス -model model.dat

 

テスト

テスト用のデータは,暗号文を作成します。

I@wetothsc.

 

テストをする際には,以下のコマンドを利用します。

kytea -model model.dat < テストファイルパス > 保存先のファイル名指定

 

データセット

今回使用するデータセットは,学習用・テスト用それぞれ研究室のコーパスを利用しました。ですので,お見せすることはできませんが,それぞれ先頭から1000文章ずつ選んで利用しました。

 

モデリング

パラメータ設定等は,KyTeaデフォルトの設定を利用しました。

 

コード解説

以下では,部分に分けてコードを解説していきたいと思います。

データ作成のパラメータ設定

train_nm = 1000
test_nm = 1000

txt_dir = "hoge.txt"

今回は,大きなコーパス”hoge.txt”から1000文章ずつを先頭から選ぶことにします。

 

with open(txt_dir) as f:
  output = list([])
  for l in f:
    w_list = l.strip().split(' ')
    ab_w_list = []
    for w in w_list:
      if len(list(w)) == 1:
        ab_w_list.append(''.join(list(w)[:2]) + '@/' + w + "@")  
      else:
        ab_w_list.append(''.join(list(w)[:2]) + '/' + w)
    output.append(' '.join(ab_w_list))

各単語ごとに「1文字であれば@をつけ」,頭の2文字をアノテーションとして後ろに付け加えます。

 

with open(txt_dir) as f:
  output2 = list([])
  for l in f:
    w_list = l.strip().split(' ')
    ab_w_list = []
    for w in w_list:
      if len(list(w)) == 1:
        ab_w_list.append(''.join(list(w)[:1]) + "@")  
      else:
        ab_w_list.append(''.join(list(w)[:2]))
    output3.append(' '.join(ab_w_list))

同様のことをします。今回は,アノテーションなしのテストデータ(暗号文)を作成します。

今回は,ダミー「@」を導入していますので,2文字ずつ読み取る前提で2文字ずつスペースを入れた暗号文を作成しています。そのため,単語分割をする必要はありません。

 

for i in range(len(output)-1):
  if ("&" in output[i]) or ("//" in output[i]):
    output[i] = 0

for i in range(len(output3)-1):
  if ("&" in output3[i]) or ("//" in output3[i]):
    output3[i] = 0

output = [output[i] for i in range(len(output)) if output[i] != 0]
output3 = [output3[i] for i in range(len(output3)) if output3[i] != 0]

注意点として,KyTeaでは「&」や「/」を含む文章をうまく処理できないため,あらかじめ除いておく必要があります。

 

training_data = output[:train_nm]
training_test_data = output3[:train_nm]
test_data = output[train_nm:train_nm + test_nm]
test_data_original = output3[train_nm:train_nm + test_nm]

冒頭に設定したパラメータの文だけスライスして取ってきます。

 

training_data = '\n'.join(training_data)
training_test_data = '\n'.join(training_test_data)
test_data = '\n'.join(test_data)
test_data_original = '\n'.join(test_data_original)

リストを1つの文字列型として合体させます。

 

file = open('training_data.txt', 'w') 
file.write(training_data)
file.close

file = open('training_test_data.txt', 'w') 
file.write(training_test_data)
file.close

file = open('test_data.txt', 'w') 
file.write(test_data)
file.close

file = open('test_data_original.txt', 'w') 
file.write(test_data_original)
file.close

作成したデータを,それぞれテキストファイルとして保存します。

 

モデルの学習

train-kytea -full training_data.txt -model model.dat

モデルを学習させます。フルアノテーションできていますので,「-full」を指定します。

 

モデルのテスト

kytea -model model.dat -nows -notag 2 -notag 3 < training_test_data.txt > result_original.full

先ほど学習したモデルを利用して,実際に解析を行います。「-nows」を指定することで単語分割を行わないように指定します。「-notag 2」「-notag 3」を指定することで,無駄なタグ推定を行わないように指定します。

 

txt_dir1 = "result_original.full"
txt_dir2 = "training_data.txt"
txt_dir3 = "result.full"
txt_dir4 = "test_data.txt"

with open(txt_dir1) as f:
  result = list([])
  for l in f:
    w_list = l.strip().split(' ')
    ab_w_list = []
    for w in w_list:
      ab_w_list.append(''.join(list(w)[3:]))
    result.append(ab_w_list)

with open(txt_dir2) as f:
  result2 = list([])
  for l in f:
    w_list = l.strip().split(' ')
    ab_w_list = []
    for w in w_list:
      ab_w_list.append(''.join(list(w))[3:])
    result2.append(ab_w_list)

with open(txt_dir3) as f:
  result3 = list([])
  for l in f:
    w_list = l.strip().split(' ')
    ab_w_list = []
    for w in w_list:
      ab_w_list.append(''.join(list(w)[3:]))
    result3.append(ab_w_list)

with open(txt_dir4) as f:
  result4 = list([])
  for l in f:
    w_list = l.strip().split(' ')
    ab_w_list = []
    for w in w_list:
      ab_w_list.append(''.join(list(w)[3:]))
    result4.append(ab_w_list)

結果を読み取ります。3文字目から推定結果が出力されているため,[3:]としてスライスしています。賢明な読者の方々は,ぜひ繰り返し構文を利用してください。

 

score = 0
for i in range(len(result)):
  for j in range(len(result[i])):
    if result[i][j] == result2[i][j]:
      score += 1
    else:
      print(result[i][j])


count_all_list = [len(v) for v in result]
count_all = 0
for i in range(len(count_all_list)):
  count_all += count_all_list[i]

accuracy = score/count_all

print("accuracy:" + str(accuracy*100) + "%")

score = 0
for i in range(min([len(result3), len(result4)])):
  for j in range(min([len(result3[i]), len(result4[i])])):
    if result3[i][j] == result4[i][j]:
      score += 1
      
count_all_list = [len(v) for v in result3]
count_all = 0
for i in range(len(count_all_list)):
  count_all += count_all_list[i]

accuracy = score/count_all

print("accuracy:" + str(accuracy*100) + "%")

Accuracyを計算します。今回は,「(合っている単語数)/(全単語数)」を定量的指標として利用しました。

 

結果

Train_accuracy:99.0%
Test_accuracy:53.2%

圧倒的過学習でした。間違えた単語を見てみると,「Th」「Ex」「Pr」から始まる単語が多かったです。これらの単語も複数の出力候補をソフトに決定するためには,モデリングの手法を変更する必要がありそうです。デフォルト設定で識別器はSVMを利用していますので,改善の余地はありそうです。

 

もし理論にモヤモヤがあれば

こちらの参考書は,PRMLよりも平易に機械学習全般の手法について解説しています。おすすめの1冊になりますので,ぜひお手に取って確認してみてください。

 

全コード

train_nm = 1000
test_nm = 1000

txt_dir = "temp.en"

with open(txt_dir) as f:
  output = list([])
  for l in f:
    w_list = l.strip().split(' ')
    ab_w_list = []
    for w in w_list:
      if len(list(w)) == 1:
        ab_w_list.append(''.join(list(w)[:2]) + '@/' + w + "@")  
      else:
        ab_w_list.append(''.join(list(w)[:2]) + '/' + w)
    output.append(' '.join(ab_w_list))

with open(txt_dir) as f:
  output3 = list([])
  for l in f:
    w_list = l.strip().split(' ')
    ab_w_list = []
    for w in w_list:
      if len(list(w)) == 1:
        ab_w_list.append(''.join(list(w)[:1]) + "@")  
      else:
        ab_w_list.append(''.join(list(w)[:2]))
    output3.append(' '.join(ab_w_list))

for i in range(len(output)-1):
  if ("&" in output[i]) or ("//" in output[i]):
    output[i] = 0    

for i in range(len(output3)-1):
  if ("&" in output3[i]) or ("//" in output3[i]):
    output3[i] = 0

output = [output[i] for i in range(len(output)) if output[i] != 0]
output3 = [output3[i] for i in range(len(output3)) if output3[i] != 0]

training_data = output[:train_nm]
training_test_data = output3[:train_nm]
test_data = output[train_nm:train_nm + test_nm]
test_data_original = output3[train_nm:train_nm + test_nm]

training_data = '\n'.join(training_data)
training_test_data = '\n'.join(training_test_data)
test_data = '\n'.join(test_data)
test_data_original = '\n'.join(test_data_original)

file = open('training_data.txt', 'w') 
file.write(training_data)
file.close

file = open('training_test_data.txt', 'w') 
file.write(training_test_data)
file.close

file = open('test_data.txt', 'w') 
file.write(test_data)
file.close

file = open('test_data_original.txt', 'w') 
file.write(test_data_original)
file.close

txt_dir1 = "result_original.full"
txt_dir2 = "training_data.txt"

with open(txt_dir1) as f:
  result = list([])
  for l in f:
    w_list = l.strip().split(' ')
    ab_w_list = []
    for w in w_list:
      ab_w_list.append(''.join(list(w)[3:]))
    result.append(ab_w_list)

with open(txt_dir2) as f:
  result2 = list([])
  for l in f:
    w_list = l.strip().split(' ')
    ab_w_list = []
    for w in w_list:
      ab_w_list.append(''.join(list(w))[3:])
    result2.append(ab_w_list)

score = 0
for i in range(len(result)):
  for j in range(len(result[i])):
    if result[i][j] == result2[i][j]:
      score += 1
    else:
      print(result[i][j])


count_all_list = [len(v) for v in result]
count_all = 0
for i in range(len(count_all_list)):
  count_all += count_all_list[i]

accuracy = score/count_all

print("accuracy:" + str(accuracy*100) + "%")

txt_dir3 = "result.full"
txt_dir4 = "test_data.txt"

with open(txt_dir3) as f:
  result3 = list([])
  for l in f:
    w_list = l.strip().split(' ')
    ab_w_list = []
    for w in w_list:
      ab_w_list.append(''.join(list(w)[3:]))
    result3.append(ab_w_list)

with open(txt_dir4) as f:
  result4 = list([])
  for l in f:
    w_list = l.strip().split(' ')
    ab_w_list = []
    for w in w_list:
      ab_w_list.append(''.join(list(w)[3:]))
    result4.append(ab_w_list)

score = 0
for i in range(min([len(result3), len(result4)])):
  for j in range(min([len(result3[i]), len(result4[i])])):
    if result3[i][j] == result4[i][j]:
      score += 1
      
count_all_list = [len(v) for v in result3]
count_all = 0
for i in range(len(count_all_list)):
  count_all += count_all_list[i]

accuracy = score/count_all

print("accuracy:" + str(accuracy*100) + "%")

COMMENT

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です