博士論文提出完了

長いことブログを更新していなかったですが、無事、博士論文を提出することができました。一段落。
あとは、年明け1月下旬から2月中旬ごろに行われる口述試験をパスすれば、博士後期課程を修了して、博士号を取得できる。

書き始めた頃は、100ページ程度を目指して書いていたが、書き進めるに連れて書くことがいっぱい出てきて、Chapter 2のLiterature Reviewを書き終えた時点で64ページ、Chapter 3のMethodologyを書き終えた時点で94ページになってしまい、最終的には本文242ページ、付録や目次等すべて含めて395ページになってしまい、かなり分厚くなった。教務課に提出するには、3部必要なので、これまた印刷が大変だった。

年内は、ちょっと休憩しよう。

テキスト内のパラグラフ数と各パラグラフ内の文数を算出

表題の通り、あるコーパス内に収録されているテキストファイルのパラグラフ数と各パラグラフ内に含まれている文の数を算出する必要が生じたので、いつものようにpythonで処理スクリプトを書いてみた。
テキストファイルは、一文一行で収録されており、パラグラフは空白行で区別されている。したがって、テキストファイル内の空白行の数を数え、そこに1を足すことで、そのテキストファイルのパラグラフの数が算出できる。
たとえば、以下のようなテキストファイル(コーパスから部分抜粋)の場合は、空白行が2つなので、パラグラフの数は、3つとなる。

Today the trend of teaching English to small children is spreading in Japan.
Do you think whether it is good or not?
There are some merits and demerits in this point.
Begin with considering three merits of teaching English early.

First, it is often said that it is better to learning foreign language in early stage.
Children have a good ability to learn anything and the older they get, the less they can memorize things.
Regard with learning language, this is most prominent.
In addition that, English is easy language in the world.
So, it is good for children to learn English as early as they can.

Second, the needs of English has become more strong.
These days, a lot of foreigners have come to Japan and the chance for Japanese children to communicate with them is increasing.
If you meet a classmate who can only speak English and you want to make friends with him, you must speak English.

次に、各パラグラフに含まれている文の数を算出したい。
1パラグラフ目の文の数は、最初の空白行が何行目にあるか特定し(上の例の場合だと5行目)、そこから1を引けば1パラグラフ目に含まれている文の数が算出できる。
2パラグラフ目の文の数も同様に、2つ目の空白行から1つ目の空白行の差を出し、そこから1を引けば、2つ目のパラグラフの文の数が算出できる。上の例の場合だと、2つ目の空白行が11行目にあるので、11から5の差を出し、そこから1を引けば5という数字が得られので、2パラグラフ目の文の数は5ということになる。

というわけで、パラグラフの数と各パラグラフに含まれている文の数を算出するプログラムをpythonで書いてみた。パラグラフが一つしかないテキストの場合は、単に文の総数を記録して、パラグラフの数は1とした。

#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys,re,glob,numpy as np

argvs = sys.argv
argc = len(argvs)

fw = open(sys.argv[2] + ".csv", 'w')
fw.write("Filename" + "," + "Sum of Sentences" + "," + "Sum of Paragraphs" + "," + "S in 1st" + ","+ "S in 2nd" + ","+ "S in 3rd" + ","+ "S in 4th" + ","+ "S in 5th" + ","+ "S in 6th" + ","+ "S in 7th" + ","+ "S in 8th" + "," + "S in 9th" + "," + "S in 10th" + "," + "S in 11th" + ","+ "S in 12th" + ","+ "S in 13th" + ","+ "S in 14th" + ","+ "S in 15th" + ","+ "S in 16th" + ","+ "S in 17th" + ","+ "S in 18th" + "," + "S in 19th" + "," + "S in 20th" + "\n")

for fn in sorted(glob.glob(sys.argv[1] + "*.txt")):
	filename = fn.split('/')[-1]
	with open(fn, 'r') as fn:
		files = fn.read()
		div = len(re.findall(r'^\n', files, flags = re.MULTILINE))
		if div != 0:
			regrep = re.sub('^\n', '%P\n', files, flags = re.MULTILINE).split("\n")
			numP = np.array([i for i, x in enumerate(regrep) if x == "%P"])
			numPS = [j for j in numP][0]
			numSEP = np.diff(numP) - 1
			L = [k for k in numSEP]
			L.insert(0, numPS)
			sumsens = sum(L)
			L.insert(0, div)
			result = ",".join(repr(l) for l in L)
			fw.write(filename + "," + str(sumsens) + "," + str(result) + "\n")
		else:
			sumsens2 = len([m.strip() for m in files.split("\n")])
			fw.write(filename + "," + str(sumsens2) + "," + str(1) + "," + str(sumsens2) + "\n")
fw.close()

もう少し短く簡潔に書けそうだが、ほしい数字を算出できたので良しとする。

引数は、対象フォルダ名と結果出力先(csv形式)の2つ。
出力形式は、ファイル名、総文数、総パラグラフ数、1パラ目の文数、2パラ目の文数、3パラ目の文数、、、という具合(見出しがちょっと雑。。だけどわかればいいのでとりあえず気にしないことにして、とりあえず20段落目までは見出しをつけた)。

出力例は、以下の通り。

Filename,Sum of Sentences,Sum of Paragraphs,S in 1st,S in 2nd,S in 3rd,S in 4th,S in 5th,S in 6th,S in 7th,S in 8th,S in 9th,S in 10th,S in 11th,S in 12th,S in 13th,S in 14th,S in 15th,S in 16th,S in 17th,S in 18th,S in 19th,S in 20th
JPN501.txt,25,4,9,5,3,8
JPN502.txt,29,5,7,10,6,0,6
JPN503.txt,8,2,4,4
JPN504.txt,23,4,8,4,7,4
JPN505.txt,19,2,13,6
JPN506.txt,18,4,1,3,6,8
JPN507.txt,24,4,1,10,4,9
JPN508.txt,16,4,1,7,4,4
JPN509.txt,18,5,1,4,3,4,6
JPN510.txt,1,1,1
JPN511.txt,25,8,1,4,5,3,3,1,4,4
JPN512.txt,22,4,1,4,8,9
JPN513.txt,30,8,1,1,10,4,1,3,6,4
JPN514.txt,6,2,1,5
JPN515.txt,22,5,1,4,8,4,5
JPN516.txt,18,4,1,3,8,6
JPN517.txt,29,7,1,3,7,6,4,4,4
JPN518.txt,24,5,1,7,5,7,4
JPN519.txt,27,4,1,8,9,9
JPN520.txt,19,4,1,4,8,6

こんな感じで、まぁ、ええか。
平成元年生まれなので、平成最後の日にブログを書こうと急いでプログラムを作成したので、結構雑になってしまったがひとまず思い取りに走ったので一安心。
令和でもマイペースにブログを更新していくことにしよう。。

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