月別アーカイブ: 2019年2月

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の意味。

ERRANTを使用するための下準備

博論関係のメモ。
GitHubERRANTというプログラムがある。

正式名称は、

ERRor ANnotation Toolkit: Automatically extract and classify grammatical errors in parallel original and corrected sentences.

ということで、例えば、学習者の作文とそれを添削した母語話者の添削文の2つを持っていれば、自動で文法的なエラーを検出し分類してくれるというもの。つまり、学習者の作文だけでなく、その学習者の作文に対して母語話者によって添削文が付与されている学習者コーパスなどがあれば、エラータグ等つけることなく、このプログラムを走らせれば文法的なエラーを検出できるという優れもの。

これは、一度使ってみようということで、まずは下準備が必要。
まずは、学習者コーパス。母語話者の添削文が付与されている学習者コーパスといえば、NICER: the Nagoya Interlanguage Corpus of English Rebornということで、今回はNICERを使ってみる。

NICERは、CHAT形式で以下のように、学習者の文と母語話者による添削文がパラレルに収録されている。

*JPN513:	education of teachers
%NTV:	Education of Teachers
%COM:	
%par:
*JPN513:	Let's think about education of teachers in Japan.
%NTV:	Let's think about the education of teachers in Japan.
%COM:	
%par:
*JPN513:	Firstly, in Japan how do people become teachers?
%NTV:	First, in Japan, how do people become teachers?
%COM:	The "-ly" ending after numbers is old-fashioned. Just say "First", "Second", etc.
*JPN513:	I know two ways.
%NTV:	OK
%COM:	
*JPN513:	One is to enter school for growing teachers.
%NTV:	One is to enter a teacher's college.
%COM:	
*JPN513:	The other is to study education besides the major.
%NTV:	The other is to study education in addition to another major.
%COM:	
*JPN513:	Near here Aichi education college are there.
%NTV:	NG
%COM:	

後略

ERRANTのreadmeを参照すると、学習者の作文と母語話者の作文は別々のファイルに保存されていなければならないらしい。しかし、NICERの収録形式では、学習者の作文の直後にそれに対応する母語話者の作文が付与されている。このままではERRANTを使用することができないので、学習者の作文は学習者用のテキストファイルに保存し、母語話者の作文は母語話者用のテキストファイルに保存することにした。もちろん手作業でやっていると(できないこともないが。。)莫大な時間がかかってしまうので、Pythonでプログラムを作成してサクッと処理することにした。

学習者の作文は、

*JPN513:	Let's think about education of teachers in Japan.
*JPN513:	Firstly, in Japan how do people become teachers?

といったように、文の先頭に学習者のIDが付与されているので、これを見つけ出して、抽出すれば良い。
母語話者の作文も同様に、

%NTV:	Let's think about the education of teachers in Japan.
%NTV:	First, in Japan, how do people become teachers?

といったように、「%NTV」という記号が先頭に付与されているので、これを見つけ出して抽出すれば良い。

案外簡単そうに思える。実際これだけなら簡単。抜き出したそれぞれの作文は、別々のファイルに保存したいので、学習者の作文を抽出したファイルは、末尾に「_ori」、母語話者の作文を抽出したファイルは、「_cor」を末尾につけることにした。
実際に、Pythonでプログラムを書いてみた。

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

import sys,re,glob

argvs = sys.argv
argc = len(argvs)

file_lists = sorted(glob.glob(sys.argv[1] + '*.txt'))

for filename in file_lists:
	fn = filename.strip("test/"".txt")
	fw = open(str(fn) + "_ori.txt", 'w')
	fw2 = open(str(fn) + "_cor.txt", 'w')
	with open(filename, 'r') as fn:
		files = fn.read()
		for i in files:
			text = re.findall('(\*.+)', files, re.IGNORECASE)
			text_ori = "\n".join(text)
			text2 = re.findall('(%NTV.+)', files, re.IGNORECASE)
			text_cor = "\n".join(text2)
		fw.write(str(text_ori))
		fw2.write(str(text_cor))
	fw.close()
	fw2.close()

学習者の作文と母語話者作文の抽出は、以下のような感じ。

学習者
*JPN513:	education of teachers
*JPN513:	Let's think about education of teachers in Japan.
*JPN513:	Firstly, in Japan how do people become teachers?
*JPN513:	I know two ways.
*JPN513:	One is to enter school for growing teachers.
*JPN513:	The other is to study education besides the major.
*JPN513:	Near here Aichi education college are there.
母語話者添削文
%NTV:	Education of Teachers
%NTV:	Let's think about the education of teachers in Japan.
%NTV:	First, in Japan, how do people become teachers?
%NTV:	OK
%NTV:	One is to enter a teacher's college.
%NTV:	The other is to study education in addition to another major.
%NTV:	NG

これでやりたいことはできた、、かのように見えるが、問題がある。
話者記号の「*JPN513」の部分と「%NTV」の部分が要らない。これだけなら、プログラムを少し修正すれば対応可能(正規表現のグループ化を話者記号の部分だけ「?:」を使って無効にする)だが、文法的に問題のない作文には、母語話者が「OK」と書いている。これが厄介。
ERRANTでは、母語話者の添削文と学習者の文を比較してエラーを検出するので、学習者が文法的に問題のない文を書いているのであれば、文法的なエラーがないということを認識させるために、母語話者の作文も「OK」ではなく、学習者の作文と全く同じでなければならない。

というわけで、母語話者が添削文を付与する箇所に「OK」と書いていれば、その箇所に学習者の文をそのまま挿入するという処理を付け加えることにした。さらに、上で書いたプログラムでは、カレントディレクトリに結果のファイルが保存されるので、カレントディレクトリではなくて、新しいディレクトリを作成して、そこに結果のファイルを保存することにした。

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

import sys,os,re,glob

argvs = sys.argv
argc = len(argvs)

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

for fn in sorted(glob.glob(sys.argv[2] + '*.txt')):
	fn2 = fn.strip(sys.argv[2] + ".txt")
	fw_ori = open(sys.argv[1] + fn2 + "_ori.txt", 'w')
	fw_cor = open(sys.argv[1] + fn2 + "_cor.txt", 'w')
	with open(fn, 'r') as fn3:
		files = fn3.readlines()
	for i in files:
		original = re.findall(r'(?:\*JPN\d\d\d:\t)(\w.+\n)', i, re.IGNORECASE)
		if len(original) != 0:
			result_ori = original[0]
			fw_ori.write(result_ori)
		correct = re.findall(r'(?:%NTV:\t)(\w.+\n)', i, re.IGNORECASE)
		if len(correct) != 0:
			cor_rep = [j.replace('OK\n', result_ori) for j in correct]
			fw_cor.write(cor_rep[0])
	fw_ori.close()
	fw_cor.close()

使い方は、MacならTerminal上で、WindowsならCygwin上などで

python3 プログラム名 保存先ディレクトリ名 抽出対象ディレクトリ名

なので、上のプログラムをextraction.pyという名前で保存するとし、保存先のディレクトリ名を「result」、NICERの保存場所が「NICER」とすると、

python3 extraction.py result/ NICER/

これで走るはず。

学習者の作文と母語話者作文の抽出は、以下のような感じ。

学習者
education of teachers
Let's think about education of teachers in Japan.
Firstly, in Japan how do people become teachers?
I know two ways.
One is to enter school for growing teachers.
The other is to study education besides the major.
Near here Aichi education college are there.
母語話者添削文
Education of Teachers
Let's think about the education of teachers in Japan.
First, in Japan, how do people become teachers?
I know two ways.
One is to enter a teacher's college.
The other is to study education in addition to another major.
NG

最初に書いたプログラムだと、「I know two ways」の部分が、母語話者の添削文だと「OK」だったのが、新しいプログラムでは、しっかりと学習者が書いた正しい文に書き換わっている。
これで下準備は完了したので、後はERRANTを使っていろいろやってみることにする。

添削文の「NG」については、これはもうどうしようもないのでとりあえずそのままにしておこう。。

catコマンドで改行記号確認

WindowsとMacintoshの両方で、テキストファイルをいじっていると改行記号が異なるためWindowsとMacintoshでテキストファイルの表示が異なる場合があり困ることが多い。
Macの方では、ちゃんと改行されているのに、Windowsの方のテキストエディタで開くと思ったように改行されていなかったり。

今回も博論の関係で、英文を添削業者にテキストファイルで提出したのだが、こちらがMacであちらがWindowsだったため、業者が開いたテキストファイルが、こちらの思ったように改行されていなくて困った。
なので、Macの改行記号をWindowsの改行記号に変更し、ちゃんと変更されているのかを確認する方法を備忘録のため書いておく。

テキストエディタで改行記号を確認できるエディタは、例えば、Windowsだとサクラエディタがあるが、Terminal上で改行記号を確認するときは、いつもcatコマンドを使う。

cat -e filename.txt

これだけで、改行記号を確認できる。
改行コードと改行記号、説明、対応する正規表現は以下の通り

改行コード 改行記号 説明 正規表現
LF $ Line Feed(Unix系全般) \n
CR ^M Carriage Return(Mac OS 9以前) \r
CRLF ^M$ 上記2つの合わせ技(Windows系) \r\n

こんな感じでTerminal上で確認できる。この例だと、改行記号はすべて^$Mになっているので、CRLF、つまりWindows対応の改行である。
改行記号の変換方法は、いろいろあるようだが、perlで正規表現を使って置換してやれば良い。

CRLFから、LF

perl -pi -e 's/\r\n/\n/g' *.txt

LFから、CRLF

perl -pi -e 's/\n/\r\n/g' *.txt

案外簡単。

参考リンク
https://en.wikipedia.org/wiki/Newline