ERRANTを使ってみる

前回の投稿では、GitHubで配布されているERRANTを使うための下準備について書いたが、下準備ができたので実際に使ってみる。Python3系とpipがインストールされている前提で話を進める。

ERRANTのreadmeを読むと、Python3spaCynltkが必要らしい。新たにインストールが必要なのはspaCynltkの2つ。spaCyとnltkはTerminal上からインストールできるので、以下のコマンドでサクッとインストールする。

pip3 install -U spacy==1.9.0
python3 -m spacy download en
pip3 install -U nltk

上の方法でエラーが出る場合は、AnacondaでPython3系をインストールして、以下を実行すれば回避できるはず。

sudo conda install -c conda-forge spacy
sudo python3 -m spacy download en
pip3 install -U nltk

ERRANTでは学習者の作文とそれを添削した母語話者の作文を使用する。前回の投稿で、NICERから学習者の作文と母語話者の添削文をそれぞれ抜き出して、別々のファイルに保存する方法を紹介したので、今回はそれらのファイルがある前提で話を進める。前回の例では、JPN513.txtから、学習者だけの作文が収録されたJPN513_ori.txtと母語話者の添削文だけが収録されたJPN513_cor.txtを作成したので、これらのファイルを使用する。母語話者の添削文は最低一つあれば大丈夫なので、今回は一つだけ使う。
Terminal上で以下を実行する。

python3 parallel_to_m2.py -orig JPN513_ori.txt -cor JPN_cor.txt -out result.m2

結果ファイルのresult.m2を開くと、m2フォーマットで以下のような感じで結果が収録されている。

S education of teachers
A 0 1|||R:ORTH|||Education|||REQUIRED|||-NONE-|||0
A 2 3|||R:ORTH|||Teachers|||REQUIRED|||-NONE-|||0

S Let's think about education of teachers in Japan.
A 3 3|||M:DET|||the|||REQUIRED|||-NONE-|||0

S Firstly, in Japan how do people become teachers?
A 0 1|||R:VERB|||First,|||REQUIRED|||-NONE-|||0
A 2 3|||R:NOUN|||Japan,|||REQUIRED|||-NONE-|||0

S I know two ways.
A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0

S One is to enter school for growing teachers.
A 4 8|||R:OTHER|||a teacher's college.|||REQUIRED|||-NONE-|||0

中略

S I think some teachers should teach subjects they are good at, some teachers support students in the aspects of health of hearts,  and others support schools in the aspects of the economy.
A 13 13|||M:VERB:TENSE|||should|||REQUIRED|||-NONE-|||0
A 15 16|||R:PREP|||with|||REQUIRED|||-NONE-|||0
A 16 22|||R:OTHER|||their emotional health,|||REQUIRED|||-NONE-|||0
A 24 24|||M:VERB:TENSE|||should|||REQUIRED|||-NONE-|||0
A 25 27|||U:OTHER||||||REQUIRED|||-NONE-|||0
A 28 29|||R:OTHER|||actual running|||REQUIRED|||-NONE-|||0
A 31 32|||R:NOUN|||school.|||REQUIRED|||-NONE-|||0

この中でほしい情報は、エラーの種類が書いてある部分。例えば、2行目の

A 0 1|||R:ORTH|||Education|||REQUIRED|||-NONE-|||0

「R:ORTH」の部分がほしい(R:ORTHは、Orthographyのエラー)。結果ファイルを1行ずつ読み込んでいって、正規表現を使ってエラーの種類が書いてある箇所のみを抜き出せばほしい部分は手に入るので、その処理を行うPythonのスクリプトを書いた。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys,re,glob

argvs = sys.argv
argc = len(argvs)

fw_result = open(sys.argv[2] + ".csv", 'w')
fw_result.write("Filename" + "," + "ErrorType" + "," + "Frequency" + "\n")
for fn in sorted(glob.glob(sys.argv[1] + "*.m2")):
	fn2 = fn.strip(sys.argv[1] + ".m2")
	with open(fn, 'r') as fn:
		files = fn.readlines()
	for i in files:
		err = re.findall(r'(^A.+)', i)
		if len(err) != 0 and '|||noop|||' not in err[0]:
			err_cou = len(err)
			err_typ = re.findall(r'(?:^A.+\|+)(\w:\w+:?\w+)', err[0])
			err.append(err_typ)
			err.append(err_cou)
			fw_result.write(str(fn2) + "," + str(err[1]) + "," + str(err[2]) + "\n")
fw_result.close()

結果の出力は、csv形式。

Filename,ErrorType,Frequency
JPN513,['R:ORTH'],1
JPN513,['R:ORTH'],1
JPN513,['M:DET'],1
JPN513,['R:VERB'],1
JPN513,['R:NOUN'],1
JPN513,['R:OTHER'],1
JPN513,['R:OTHER'],1

中略

JPN513,['M:VERB:TENSE'],1
JPN513,['R:PREP'],1
JPN513,['R:OTHER'],1
JPN513,['M:VERB:TENSE'],1
JPN513,['U:OTHER'],1
JPN513,['R:OTHER'],1
JPN513,['R:NOUN'],1

こんな感じでcsv形式で出力されるので、これをExcelに貼り付けてPivotTableを使えばそれぞれのエラータイプごとの数が算出できる、、と思ったが、一々PivotTableを使うのは面倒なので、Pythonのスクリプトの中でファイルごとにエラータイプの数を数えて、それをcsvとして出力するスクリプトを書いてみた。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import os,sys,re,glob,collections,pandas

argvs = sys.argv
argc = len(argvs)

os.makedirs(sys.argv[2], exist_ok=True)

#カウント処理
for fn in sorted(glob.glob(sys.argv[1] + "*.m2")):
	err_typ_list = []
	fn2 = fn.strip(sys.argv[1] + ".m2")
	with open(fn, 'r') as fn:
		files = fn.readlines()
	for i in files:
		err_typ = re.findall(r'(?:^A.+\|+)(\w:\w+:?\w+)', i)
		err_typ_list.append(err_typ)
	tmp = [j for j in err_typ_list if j]
	typ_tup = list(map(tuple, tmp))
	tup_fre = collections.Counter(typ_tup)
	result = pandas.DataFrame(tup_fre, index=[fn2]).sort_index(axis = 1)
	result.to_csv(sys.argv[2] + fn2 + ".csv")
	
#結果出力
csv_list = []
for k in sorted(glob.glob(sys.argv[2] + "*.csv")):
	csv_list.append(pandas.read_csv(k))
	os.remove(k)
fin = pandas.concat(csv_list, sort = False).fillna(0).rename(columns = {'Unnamed: 0': 'Filename'})
fin.to_csv(sys.argv[2] + sys.argv[3] + ".csv", index = False)

forループの処理が一ファイル終わるごとに、pandasでDataFrameを作っていき、最後にまとめた結果をcsvに書き出しという処理をやりたかったが、うまくできなかったので、ファイルを一回処理するごとにそのファイルの結果をcsvに出力して、処理が最後まで終わればcsvファイルを結合し、一ファイルごとのcsvファイルを削除してまとめのcsvを出力するという手順になった。
使用する引数は、m2ファイルが入っているディレクトリ名、結果出力先のディレクトリ名、結果出力先のファイル名の3つ。

python3 error_detection.py m2files/ result_dir/ result

こんな感じでやればサクッと結果が出力される。試しに、NICERに収録されているJPN513からJPN520までの学習者の作文と母語話者の作文を抽出して、ERRANTを使用し、エラーの数をタイプ別に数えてみた。
結果は、

Filename,M:DET,M:NOUN,M:OTHER,M:PREP,M:VERB:TENSE,R:ADJ,R:DET,R:MORPH,R:NOUN,R:NOUN:NUM,R:ORTH,R:OTHER,R:PREP,R:PRON,R:VERB,R:VERB:FORM,R:VERB:SVA,R:VERB:TENSE,U:DET,U:NOUN,U:OTHER,U:VERB,M:ADJ,M:VERB,M:VERB:FORM,R:PART,U:VERB:TENSE,U:PREP,R:ADV,U:VERB:FORM,M:ADV,R:WO,U:PRON,M:CONJ,R:CONJ,U:ADV
JPN513,4,1.0,3.0,2.0,2.0,1.0,3.0,1.0,6,2.0,2,23.0,1,1.0,4.0,1.0,5.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
JPN514,2,0.0,2.0,0.0,0.0,0.0,0.0,0.0,1,0.0,3,0.0,1,0.0,1.0,0.0,1.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
JPN515,8,0.0,1.0,1.0,0.0,2.0,2.0,1.0,3,2.0,2,15.0,3,0.0,0.0,0.0,1.0,1.0,1.0,1.0,2.0,0.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
JPN516,6,0.0,5.0,2.0,0.0,3.0,1.0,0.0,12,1.0,1,58.0,1,2.0,12.0,1.0,1.0,1.0,0.0,0.0,2.0,1.0,0.0,1.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
JPN517,7,0.0,0.0,3.0,1.0,0.0,0.0,0.0,3,3.0,4,4.0,6,3.0,1.0,1.0,1.0,4.0,1.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0
JPN518,8,0.0,2.0,1.0,0.0,1.0,2.0,0.0,2,4.0,5,14.0,1,1.0,3.0,1.0,0.0,0.0,3.0,1.0,1.0,2.0,0.0,1.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0
JPN519,5,1.0,0.0,2.0,0.0,0.0,8.0,0.0,7,5.0,1,25.0,5,1.0,9.0,1.0,4.0,4.0,8.0,0.0,3.0,2.0,1.0,0.0,0.0,1.0,0.0,5.0,1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0
JPN520,1,1.0,2.0,2.0,0.0,0.0,1.0,1.0,5,0.0,1,26.0,1,5.0,6.0,2.0,3.0,1.0,4.0,1.0,3.0,1.0,0.0,2.0,0.0,0.0,0.0,2.0,0.0,1.0,2.0,0.0,0.0,1.0,2.0,1.0

こんな感じ。csvファイルに出力されるので、分析もしやすい。
前回問題になった母語話者の添削文「NG」については、「R:OTHER」に分類されるようで、readmeを見ると「R:OTHER」は、

Edits that are not captured by any rules are classified as OTHER. They are typically edits such as [*at* (PREP) -> *the* (DET)], which have perhaps been improperly aligned, or else multi-token edits such as [*at his best* -> *well*] which are much more semantic in nature.

とのことなので、そのままでもいいか。。
エラーコードの詳細は、Bryant et al. (2017)を参照。エラーコードの先頭には、M、R、Uという記号がついているが、それぞれ、Missing、Replacement、Unnecessaryの意味。