「Python」カテゴリーアーカイブ

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」については、これはもうどうしようもないのでとりあえずそのままにしておこう。。

Pythonで任意の表現を任意のディレクトリ内で一括検索

任意のディレクトリ内にあるtxtファイル全てを対象に任意の文字列を検索したい。
Terminal上で、grepすれば済む話だけど、検索したい文字列が大量にあって、個々の頻度も知りたい、、、と思ってスクリプトを書いた。

検索したい文字列を予め一行一表現で、txtファイルにまとめておいて、検索対象のディレクトリとは別に用意しておく。
結果は、タブ区切りでファイル名、文字列、頻度の順で出力。

以下、スクリプト

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

import sys,os,re,glob

argvs = sys.argv
argc = len(argvs)

file_list = sorted(glob.glob(sys.argv[1] + '*.txt'))
with open(sys.argv[2] + ".txt", 'r') as DM:
	DMs = DM.read().splitlines()
	
fw = open(sys.argv[3] + ".txt", 'w')
fw.write("Filename" + "\t" + "DiscourseMarker" + "\t" + "Frequency" + "\n")

for filename in file_list:
	fn = filename.strip("files/"".txt")
	with open(filename,'r') as f:
		files = f.read()
		for DMss in DMs:
			fre = len(re.findall(r'(\b)' + DMss + r'(\b)', files, re.IGNORECASE))
			mydict = {}
			mydict[DMss] = fre
			for DMss in mydict:
				DMlist = DMss
				print(fn)
				print(mydict)
				fw.write(str(fn) + "\t" + str(DMlist) + "\t" + str(mydict[DMss]) + "\n")
fw.close()

もうちょっと簡単に書けそうな気もするが、、、動くからいいや。

もともとは、Discourse Markerのリストを作って、そのリスト内の表現の頻度をファイル別で知りたかった。
ので、出力の見出しもFilename、DiscourseMarkerとなっている。