2. 데이터 탐색과 전처리

Open In Colab

이전 장에서는 Question Generation Task의 개념과 모델, 평가지표 등에 대해 알아보았습니다. 이번 장에서는 실습에 사용할 데이터셋을 확인해보도록 하겠습니다.

NLP 튜토리얼에서는 에세이 데이터를 활용해 학습자들을 위한 영어 문제를 생성하는 모델을 만들어 볼 것입니다. 첫번째로는 여러개의 선택지 중에서 지문 내용에 기반해 진실인 선택지 하나를 선택하는 문제이며, 두번재로는 지문 내용에 기반해 When, What, Why, Who, Where, 그리고 How를 묻는 문제입니다.

문제 생성에 기반이 되는 지문으로는 2014 CoNLL Shared Task 데이터와 BEA-2019 Shared Task 데이터를 사용할 것입니다. 각각에 대한 내용은 2.1절과 2.2절에서 세부적으로 다뤄보겠습니다.

위 데이터를 통해 생성할 문제형식은 모두 4지선다형입니다. 4지선다형은 채점자 입장에서 평가하기가 용이하며, 시험자는 반복적으로 4지선다형 문제를 풀어가면서 원본 지문에 대한 개념을 체득할 수 있습니다.

2.1절에서는 2014 CoNLL Shared Task 데이터에 대한 탐색과 전처리를 진행할 것이며 2.2절에서는 BEA-2019 Shared Task 데이터에 대한 탐색과 전처리를 진행해보겠습니다.

2.1 2014 CoNLL Shared Task Dataset

Conference on Computational Natural Language Learning (CoNLL)은 매년 자연어처리 관련 기술에 대해 논하는 학회입니다. 논문 발표와 더불어 매년 자연어 처리와 관련된 Shared Task를 운영하고 있습니다. Shared Task는 특정 기간 동안 여러 팀이 참가해서 주최측에서 제시한 문제를 해결하는 과제로써, 데이터 경진대회와 유사합니다. CoNLL에서 운영한 모든 Shared Task는 https://conll.org/previous-tasks에서 확인 가능합니다.

2013년, 2014년에는 Grammer Error Correction(GEC) 과제를 Shared Task로 운영했습니다. GEC는 에세이내의 문법 오류 위치를 탐지한 후 올바른 단어로 수정하는 알고리즘을 구축하는 과제였습니다.

이번 튜토리얼에서 사용할 데이터는 CoNLL Shared Task가 종료된 후 주최측에서 공개한 라벨링된 시험 데이터셋 입니다. Shared Task에서 공식적으로 사용된 훈련 데이터인 NUCLE corpus는 NUS Natural Language Processing Group에 신청서를 보내야 얻을 수 있지만, 시험 데이터셋은 누구나 열람할 수 있게 공개가 되어 있기 때문입니다.

2014년도 GEC의 시험 데이터셋은 총 50개의 에세이로 이뤄져 있습니다. 25명의 비영어권 NUS 학생(Non-native speaker of English)이 표 2.1에 있는 2개의 주제에 대해 각각 하나의 에세이를 제출해서 총 50개 입니다. 해당 에세이에 대해 2명의 영어 원어민이 문법 교정, 즉 데이터 라벨링을 실시했습니다.

ID

Prompt

1

“The decision to undergo genetic testing can only be made by the individual at risk for a disorder.
Once a test has been conducted and the results are known, however, a new, family-related ethical dilemma is born:
Should a carrier of a known genetic risk be obligated to tell his or her relatives?”
Respond to the question above, supporting your argument with concrete examples.

2

While social media sites such as Twitter and Facebook can connect us closely to people in many parts of the world,
some argue that the reduction in face-to-face human contact affects interpersonal skills.
Explain the advantages and disadvantages of using social media in your daily life/society

  • 표 2.1 시험 데이터셋 구축에 사용한 에세이 주제

2.1.1 CoNLL 데이터 탐색

먼저 아래 코드를 활용해 https://www.comp.nus.edu.sg/~nlp/conll14st.html 에 공개된 시험 데이터셋을 Colab환경에 다운로드 받습니다. 리눅스의 wget명령어를 활용해 제공된 다운로드 링크로 부터 tar.gz파일을 받을 수 있습니다. 이때 -O 옵션을 통해 파일명을 conll2014.tar.gz로 변경해서 다운로드 받아보도록 하겠습니다.

!wget -O 'conll2014.tar.gz' https://www.comp.nus.edu.sg/~nlp/conll14st/conll14st-test-data.tar.gz
--2021-04-18 12:37:53--  https://www.comp.nus.edu.sg/~nlp/conll14st/conll14st-test-data.tar.gz
Resolving www.comp.nus.edu.sg (www.comp.nus.edu.sg)... 45.60.31.225
Connecting to www.comp.nus.edu.sg (www.comp.nus.edu.sg)|45.60.31.225|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 643482 (628K) [application/x-gzip]
Saving to: ‘conll2014.tar.gz’

conll2014.tar.gz    100%[===================>] 628.40K   334KB/s    in 1.9s    

2021-04-18 12:37:57 (334 KB/s) - ‘conll2014.tar.gz’ saved [643482/643482]

  • 그림 2.1 다운로드 된 tar.gz 파일

Colab 파일 경로에 그림 2.1과 같이 conll2014.tar.gz파일이 다운로드 된것을 확인할 수 있습니다. 다음으로는 해당 파일을 리눅스의 tar명령어를 활용해 압축 해제 하겠습니다. 아래 코드에서 사용한 tar명령어의 옵션은 다음과 같습니다. [4]

  • x – tar 파일 압축 해제

  • v – 압축하는 과정 표시

  • f – tar 파일명 입력

!tar -xvf conll2014.tar.gz
conll14st-test-data/
conll14st-test-data/scripts/
conll14st-test-data/scripts/parser_feature.py
conll14st-test-data/scripts/preprocess.py
conll14st-test-data/scripts/nuclesgmlparser.py
conll14st-test-data/scripts/README
conll14st-test-data/scripts/nucle_doc.py
conll14st-test-data/scripts/preprocesscombine.py
conll14st-test-data/scripts/preprocesswithalt.py
conll14st-test-data/scripts/iparser.py
conll14st-test-data/noalt/
conll14st-test-data/noalt/official-2014.1.conll.ann
conll14st-test-data/noalt/official-2014.0.sgml
conll14st-test-data/noalt/official-2014.combined.m2
conll14st-test-data/noalt/official-2014.1.sgml
conll14st-test-data/noalt/official-2014.1.m2
conll14st-test-data/noalt/official-2014.0.conll.ann
conll14st-test-data/noalt/official-2014.0.m2
conll14st-test-data/README
conll14st-test-data/alt/
conll14st-test-data/alt/alternative-teama.sgml
conll14st-test-data/alt/alternative-teamc.sgml
conll14st-test-data/alt/alternative-teamb.sgml
conll14st-test-data/alt/official-2014.combined-withalt.m2

압축이 해제되면 conll14st-test-data폴더가 생성된 것을 확인할 수 있습니다. 해당 폴더는 그림 2.2와 같이 구성돼 있습니다.

  • 그림 2.2 conll14st-test-data 폴더 구조

각 폴더가 의미하는 내용은 다음과 같습니다.

  • alt: 참가팀에서 제출한 대체 답안이 저장된 폴더

  • noalt: 주최측의 답안이 저장된 폴더

  • scripts: 주최측에서 제공한 데이터 전처리 스크립트

2014 CoNLL Shared Task에서 참가자는 올바른 문법 용어를 예측해야 합니다. 하지만 올바른 문법 용어는 여러개가 존재할 수 있기 때문에, 주최측의 Annotator들이 모두 다룰 수가 없을 수 있습니다. 이러한 점을 고려해 대회 종료 후 참가팀으로 부터 Alternative Answer를 제출 받았고, 총 3팀이 제출해서 해당 팀들의 답안이 alt 폴더에 저장돼 있습니다.

scripts폴더 내 파일들은 파이썬 2.6.4에서 원할히 작동합니다. 하지만 최근에는 대부분 파이썬 3를 사용하기 때문에 본 튜토리얼에서는 scripts폴더에서 제공되는 함수를 사용하지 않았습니다. 또한, scripts에 있는 코드를 사용해 CoNLL Shared Task 데이터 양식에 한정된 코드를 배우는 것 보다는, 일반적으로 사용되는 BeautifulSoup, Numpy 등의 패키지로 전처리 하는 과정을 연습해보면 다른 데이터셋을 전처리 할 때도 도움이 되기 때문에 본 튜토리얼에서는 자체적으로 전처리 과정을 구성해보겠습니다. 전처리 하는 과정을 익혀봄으로써 다른 데이터셋이 주어졌을 때 유동적으로 대처하는 능력이 배양되길 바랍니다.

전처리를 하기 위해 먼저 BeautifulSoup 함수를 불러옵니다.

from bs4 import BeautifulSoup

noalt폴더 내에 있는 official-2014.0.sgml파일을 open() 함수를 활용해 엽니다.

# annotator id 0에 의해 라벨링된 파일
sgml_file = open('conll14st-test-data/noalt/official-2014.0.sgml')

그 후 read() 함수를 활용해 official-2014.0.sgml 파일의 내용물을 읽어서 conll 변수에 저장합니다.

conll = sgml_file.read()
sgml_file.close()
type(conll)
str

conll 변수 타입을 확인해보니 문자열로 저장된 것을 확인할 수 있습니다. conll 변수에 어떤 문자열이 저장됐는지 확인해보겠습니다.

conll[:2500]
'<DOC nid="1">\n<TEXT>\n<TITLE>\nKeeping the Secret of Genetic Testing\n</TITLE>\n<P>\nWhat is genetic risk? Genetic risk refers more to your chance of inheriting a disorder or disease. People get certain disease because of genetic changes. How much a genetic change tells us about your chance of developing a disorder is not always clear. If your genetic results indicate that you have gene changes associated with an increased risk of heart disease, it does not mean that you definitely will develop heart disease. The opposite is also true. If your genetic results show that you do not have changes associated with an increased risk of heart disease, it is still possible that you develop heart disease. However for some rare diseases, people who have certain gene changes are guaranteed to develop the disease. When we are diagonosed out with certain genetic disease, are we suppose to disclose this result to our relatives? My answer is no.\n</P>\n<P>\nOn one hand, we do not want this potential danger causing firghtenning affects in our families\' later lives. When people around us know that we got certain disease, their altitudes will be easily changed, whether caring us too much or keeping away from us. And both are not what we want since most of us just want to live as normal people. Surrounded by such concerns, it is very likely that we are distracted to worry about these problems. It is a concern that will be with us during our whole life, because we will never know when the \'\'potential bomb\'\' will explode.\n</P>\n<P>\nOn the other hand, if there are ways can help us to control or cure the disease, we can going through thses process from the scope of the whole family. For an example, if exercising is helpful for family potential disease, we can always look for more chances for the family to go exercise. And we keep track of all family members health conditions. At the same time, we are prepared to know when there are other members got this disease.\n</P>\n<P>\nHere I want to share Forest\'view on this issue. Although some people feel that an individual who is found to carry a dominant gene for Huntington\'s disease has an ethical obligation to disclose that fact to his or her siblings, there currently is no legal requirement to do so. In fact, requiring someone to communicate his or her own genetic risk to family members who are therefore also at risk is considered by many to be ethically dubious."\n</P>\n<P>\nNothing is absolute right or wrong. If certain disease genetic test is v'

보시다 시피 conll변수 내에 저장된 문자열에 다양한 태그(tag)들이 존재하는 것을 확인할 수 있습니다. <DOC>, <TEXT>, <TITLE> 등 의 태그들이 존재합니다. 이것은 Markup Language를 표현하는 방식과 같습니다. 파일 확장자 명에 있는 sgml는 Standard Generalized Markup Language의 약자입니다. SGML은 HTML등의 Markup Language가 따라야 하는 기준을 제시하는 메타 언어입니다.

그러므로 conll변수를 HTML 데이터를 파싱할 때 사용하는 BeautifulSoup 함수를 활용해 처리해보겠습니다. 아래 코드처럼 Markup Language 형태의 정보가 저장된 conll변수를 첫번째 파라미터로 넘기고, 두번째 파라미터에는 데이터 처리시 사용할 parser를 명시합니다. 일반적으로 BeautifulSoup에서 제공하는 html.parser를 사용하기도 하지만, 공식 문서에서는 속도를 위해 lxml사용하는 것을 추천하므로, 본 튜토리얼에서도 lxml을 활용해보겠습니다.

# 파싱하고
conllsoup = BeautifulSoup(conll, 'lxml')
type(conllsoup)
bs4.BeautifulSoup

BeautifulSoup객체로 변환된것을 확인할 수 있습니다. 가장 먼저 CoNLL 데이터셋 내에 어떤 태그들이 존재하는지 확인해보겠습니다.

tag_names = set([tag.name for tag in conllsoup.find_all()])

List Comprehension을 활용해 conllsoup내에 있는 모든 태그들을 list 내부에 저장합니다. 그 중에는 중복되는 태그명들도 있을 수 있으므로, 고유의 태그명만 확인하기 위해 set()함수를 사용해 listset 형태로 변환시켜 줍니다. 파이썬에서 set은 중복되는 원소를 가질 수 없기 때문에, 중복 원소가 있는 경우 하나의 원소만 남겨두고 나머지는 제거하게 됩니다.

tag_names의 있는 고유 태그들을 확인해보면 아래와 같습니다.

tag_names
{'annotation',
 'body',
 'comment',
 'correction',
 'doc',
 'html',
 'mistake',
 'p',
 'text',
 'title',
 'type'}
conllsoup.find('doc')
<doc nid="1">
<text>
<title>
Keeping the Secret of Genetic Testing
</title>
<p>
What is genetic risk? Genetic risk refers more to your chance of inheriting a disorder or disease. People get certain disease because of genetic changes. How much a genetic change tells us about your chance of developing a disorder is not always clear. If your genetic results indicate that you have gene changes associated with an increased risk of heart disease, it does not mean that you definitely will develop heart disease. The opposite is also true. If your genetic results show that you do not have changes associated with an increased risk of heart disease, it is still possible that you develop heart disease. However for some rare diseases, people who have certain gene changes are guaranteed to develop the disease. When we are diagonosed out with certain genetic disease, are we suppose to disclose this result to our relatives? My answer is no.
</p>
<p>
On one hand, we do not want this potential danger causing firghtenning affects in our families' later lives. When people around us know that we got certain disease, their altitudes will be easily changed, whether caring us too much or keeping away from us. And both are not what we want since most of us just want to live as normal people. Surrounded by such concerns, it is very likely that we are distracted to worry about these problems. It is a concern that will be with us during our whole life, because we will never know when the ''potential bomb'' will explode.
</p>
<p>
On the other hand, if there are ways can help us to control or cure the disease, we can going through thses process from the scope of the whole family. For an example, if exercising is helpful for family potential disease, we can always look for more chances for the family to go exercise. And we keep track of all family members health conditions. At the same time, we are prepared to know when there are other members got this disease.
</p>
<p>
Here I want to share Forest'view on this issue. Although some people feel that an individual who is found to carry a dominant gene for Huntington's disease has an ethical obligation to disclose that fact to his or her siblings, there currently is no legal requirement to do so. In fact, requiring someone to communicate his or her own genetic risk to family members who are therefore also at risk is considered by many to be ethically dubious."
</p>
<p>
Nothing is absolute right or wrong. If certain disease genetic test is very accurate and it is unavoidable and necessary to get treatment and known by others, it is OK to disclose the result. Above all, life is more important than secret.
</p>
</text>
<annotation teacher_id="8">
<mistake end_off="46" end_par="1" start_off="42" start_par="1">
<type>ArtOrDet</type>
<correction></correction>
</mistake>
<mistake end_off="125" end_par="1" start_off="118" start_par="1">
<type>Nn</type>
<correction>diseases</correction>
</mistake>
<mistake end_off="627" end_par="1" start_off="620" start_par="1">
<type>Trans</type>
<correction>However,</correction>
</mistake>
<mistake end_off="751" end_par="1" start_off="740" start_par="1">
<type>Mec</type>
<correction>diagnosed</correction>
</mistake>
<mistake end_off="754" end_par="1" start_off="751" start_par="1">
<type>Prep</type>
<correction></correction>
</mistake>
<mistake end_off="783" end_par="1" start_off="776" start_par="1">
<type>Nn</type>
<correction>diseases</correction>
</mistake>
<mistake end_off="58" end_par="2" start_off="50" start_par="2">
<type>Wci</type>
<correction>having</correction>
</mistake>
<mistake end_off="70" end_par="2" start_off="58" start_par="2">
<type>Mec</type>
<correction>frightening</correction>
</mistake>
<mistake end_off="78" end_par="2" start_off="71" start_par="2">
<type>Wform</type>
<correction>effects</correction>
</mistake>
<mistake end_off="147" end_par="2" start_off="144" start_par="2">
<type>Wci</type>
<correction>have</correction>
</mistake>
<mistake end_off="163" end_par="2" start_off="156" start_par="2">
<type>Nn</type>
<correction>diseases</correction>
</mistake>
<mistake end_off="180" end_par="2" start_off="171" start_par="2">
<type>Mec</type>
<correction>attitude</correction>
</mistake>
<mistake end_off="203" end_par="2" start_off="186" start_par="2">
<type>Vform</type>
<correction>easily change</correction>
</mistake>
<mistake end_off="219" end_par="2" start_off="213" start_par="2">
<type>Prep</type>
<correction>caring for</correction>
</mistake>
<mistake end_off="412" end_par="2" start_off="410" start_par="2">
<type>Trans</type>
<correction>and</correction>
</mistake>
<mistake end_off="516" end_par="2" start_off="512" start_par="2">
<type>Vt</type>
<correction></correction>
</mistake>
<mistake end_off="36" end_par="3" start_off="32" start_par="3">
<type>Ssub</type>
<correction>ways that</correction>
</mistake>
<mistake end_off="93" end_par="3" start_off="88" start_par="3">
<type>Vform</type>
<correction>go</correction>
</mistake>
<mistake end_off="107" end_par="3" start_off="102" start_par="3">
<type>Mec</type>
<correction>these</correction>
</mistake>
<mistake end_off="115" end_par="3" start_off="108" start_par="3">
<type>Nn</type>
<correction>processes</correction>
</mistake>
<mistake end_off="158" end_par="3" start_off="156" start_par="3">
<type>Rloc-</type>
<correction></correction>
</mistake>
<mistake end_off="196" end_par="3" start_off="193" start_par="3">
<type>V0</type>
<correction>reducing</correction>
</mistake>
<mistake end_off="279" end_par="3" start_off="277" start_par="3">
<type>Wci</type>
<correction>do</correction>
</mistake>
<mistake end_off="290" end_par="3" start_off="288" start_par="3">
<type>Rloc-</type>
<correction></correction>
</mistake>
<mistake end_off="293" end_par="3" start_off="290" start_par="3">
<type>Trans</type>
<correction>so</correction>
</mistake>
<mistake end_off="423" end_par="3" start_off="420" start_par="3">
<type>Ssub</type>
<correction>who have got</correction>
</mistake>
<mistake end_off="28" end_par="4" start_off="21" start_par="4">
<type>Npos</type>
<correction>Forests's</correction>
</mistake>
<mistake end_off="19" end_par="5" start_off="11" start_par="5">
<type>Wform</type>
<correction>absolutely</correction>
</mistake>
<mistake end_off="46" end_par="5" start_off="39" start_par="5">
<type>ArtOrDet</type>
<correction>a certain</correction>
</mistake>
<mistake end_off="54" end_par="5" start_off="47" start_par="5">
<type>Rloc-</type>
<correction></correction>
</mistake>
<mistake end_off="147" end_par="5" start_off="142" start_par="5">
<type>Wci</type>
<correction>tell</correction>
</mistake>
<mistake end_off="150" end_par="5" start_off="148" start_par="5">
<type>Prep</type>
<correction></correction>
</mistake>
<mistake end_off="237" end_par="5" start_off="231" start_par="5">
<type>Nn</type>
<correction>secrets</correction>
</mistake>
</annotation>
</doc>

각각의 태그와 태그 내의 속성이 의미하는 바는 다음과 같습니다.

  • annotation: text의 모든 문법 오류를 저장한 태그, mistake의 부모 태그

    • teacher_id: 문법 교정한 선생님 고유 번호

  • body: 모든 태그 정보가 저장된 부모 태그

  • comment:

  • correction:

  • doc: text, title, annotation정보가 저장된 부모 태그

    • nid: 문서 고유 번호

  • html: body의 부모 태그

  • mistake: type, comment, correction 정보가 저장된 부모 태그

    • end_off: 오류가 끝나는 문자열 위치

    • end_par: 오류가 끝나는 문단 위치

    • start_off: 오류가 시작하는 문자열 위치

    • start_par: 오류가 끝나는 문단 위치

  • p: 에세이의 각 문단이 저장된 태그

  • text: 학생이 쓴 에세이가 저장된 태그, titlep의 부모 태그

  • title: 에세이의 제목이 저장된 태그

  • type: 문법 오류 유형이 저장된 태그

해당 에세이를 독해 문제 지문으로 활용하기 위해선 탐지된 문법 오류를 모두 교정할 필요가 있습니다. 문법 오류가 발생한 위치와 해당 단어를 대체하기 위한 올바른 문법 단어를 알고 있으므로, 적절한 알고리즘을 활용해 에세이내의 모든 문법 오류를 대체할 수 있습니다. 해당 과정을 2.1.2절에서 확인해보겠습니다.

2.1.2 CoNLL 데이터 전처리

먼저 mistake 태그에 담긴 정보들을 확인해보겠습니다. BeautifulSoup객체의 find('태그') 함수를 사용하면 가장 첫번째로 나오는 태그를 반환해줍니다.

conllsoup.find('mistake')
<mistake end_off="46" end_par="1" start_off="42" start_par="1">
<type>ArtOrDet</type>
<correction></correction>
</mistake>

start_parend_par는 각각 text내에서 몇번째 문단에 문법 오류가 존재하는지 나타내고, start_offend_off는 해당 문단에서 몇번째 문자열에 문법 오류가 존재하는지 나타냅니다. 가령 위 예시에서는 첫번째 문단의 [42:46]위치에 있는 문자열이 문법 오류를 지니는 것입니다.

type은 문법 오류의 종류를 나타내는데, GEC문제라면 관심있게 봐야하는 태그이지만, 본 튜토리얼에서는 문법 오류를 올바른 단어로 대체해서 사용하는 것이 목적이므로 어떤 문법 오류 종류가 존재하는지 세부적으로 확인하지 않겠습니다. 궁금하신 분께서는 참고문헌 [1]을 참고하시기 바랍니다.

correction은 문법 오류를 고치기 위해서 사용해야 하는 단어를 뜻합니다. 위 예시에서는 값이 없으므로, 이 뜻은 단어를 삭제해야 문법 오류가 교정된다는 뜻입니다.

아래 코드를 확인해 어떤 단어인지 확인해보겠습니다. text태그에 존재하는 요소를 확인해보겠습니다.

conllsoup.find('text')
<text>
<title>
Keeping the Secret of Genetic Testing
</title>
<p>
What is genetic risk? Genetic risk refers more to your chance of inheriting a disorder or disease. People get certain disease because of genetic changes. How much a genetic change tells us about your chance of developing a disorder is not always clear. If your genetic results indicate that you have gene changes associated with an increased risk of heart disease, it does not mean that you definitely will develop heart disease. The opposite is also true. If your genetic results show that you do not have changes associated with an increased risk of heart disease, it is still possible that you develop heart disease. However for some rare diseases, people who have certain gene changes are guaranteed to develop the disease. When we are diagonosed out with certain genetic disease, are we suppose to disclose this result to our relatives? My answer is no.
</p>
<p>
On one hand, we do not want this potential danger causing firghtenning affects in our families' later lives. When people around us know that we got certain disease, their altitudes will be easily changed, whether caring us too much or keeping away from us. And both are not what we want since most of us just want to live as normal people. Surrounded by such concerns, it is very likely that we are distracted to worry about these problems. It is a concern that will be with us during our whole life, because we will never know when the ''potential bomb'' will explode.
</p>
<p>
On the other hand, if there are ways can help us to control or cure the disease, we can going through thses process from the scope of the whole family. For an example, if exercising is helpful for family potential disease, we can always look for more chances for the family to go exercise. And we keep track of all family members health conditions. At the same time, we are prepared to know when there are other members got this disease.
</p>
<p>
Here I want to share Forest'view on this issue. Although some people feel that an individual who is found to carry a dominant gene for Huntington's disease has an ethical obligation to disclose that fact to his or her siblings, there currently is no legal requirement to do so. In fact, requiring someone to communicate his or her own genetic risk to family members who are therefore also at risk is considered by many to be ethically dubious."
</p>
<p>
Nothing is absolute right or wrong. If certain disease genetic test is very accurate and it is unavoidable and necessary to get treatment and known by others, it is OK to disclose the result. Above all, life is more important than secret.
</p>
</text>

text태그 내에는 titlep태그가 존재합니다. title은 해당 에세이의 제목, p는 문단을 나타냅니다. 앞서 살펴본 start_parend_par의 번호는 text내에 존재하는 요소들에 하나씩 대응 되며, title을 포함해서 번호를 매깁니다. 즉, 위 예시에서 start_parend_par이 모두 0이면 Keeping the Secret of Genetic Testing에 문법 오류가 존재한다는 뜻이며, 1이면은 첫번재 p태그에서 문법 오류가 존재한다는 뜻입니다. text내에 title태그가 없고 p태그만 있다면 start_par/end_par가 0일 때 첫번째 p태그를 가르키게 됩니다.

text내의 내용을 태그 단위로 접근할 수 있게 전처리를 해보겠습니다.

conllsoup.find('text').text
'\n\nKeeping the Secret of Genetic Testing\n\n\nWhat is genetic risk? Genetic risk refers more to your chance of inheriting a disorder or disease. People get certain disease because of genetic changes. How much a genetic change tells us about your chance of developing a disorder is not always clear. If your genetic results indicate that you have gene changes associated with an increased risk of heart disease, it does not mean that you definitely will develop heart disease. The opposite is also true. If your genetic results show that you do not have changes associated with an increased risk of heart disease, it is still possible that you develop heart disease. However for some rare diseases, people who have certain gene changes are guaranteed to develop the disease. When we are diagonosed out with certain genetic disease, are we suppose to disclose this result to our relatives? My answer is no.\n\n\nOn one hand, we do not want this potential danger causing firghtenning affects in our families\' later lives. When people around us know that we got certain disease, their altitudes will be easily changed, whether caring us too much or keeping away from us. And both are not what we want since most of us just want to live as normal people. Surrounded by such concerns, it is very likely that we are distracted to worry about these problems. It is a concern that will be with us during our whole life, because we will never know when the \'\'potential bomb\'\' will explode.\n\n\nOn the other hand, if there are ways can help us to control or cure the disease, we can going through thses process from the scope of the whole family. For an example, if exercising is helpful for family potential disease, we can always look for more chances for the family to go exercise. And we keep track of all family members health conditions. At the same time, we are prepared to know when there are other members got this disease.\n\n\nHere I want to share Forest\'view on this issue. Although some people feel that an individual who is found to carry a dominant gene for Huntington\'s disease has an ethical obligation to disclose that fact to his or her siblings, there currently is no legal requirement to do so. In fact, requiring someone to communicate his or her own genetic risk to family members who are therefore also at risk is considered by many to be ethically dubious."\n\n\nNothing is absolute right or wrong. If certain disease genetic test is very accurate and it is unavoidable and necessary to get treatment and known by others, it is OK to disclose the result. Above all, life is more important than secret.\n\n'

text태그 내의 text정보만 추출하면 new line (\n) 문자가 여러 존재합니다. 문자열 기준 가장 앞과 뒤에 있는 \nstrip()함수를 통해 제거 가능합니다. 또한 중간에 있는 \n\n\n은 각 문단을 구분해주는 역할을 하므로, 해당 문자열을 기준으로 split()을 적용해 문단을 나누겠습니다.

paragraph_list = conllsoup.find('text').text.strip().split('\n\n\n')
paragraph_list
['Keeping the Secret of Genetic Testing',
 'What is genetic risk? Genetic risk refers more to your chance of inheriting a disorder or disease. People get certain disease because of genetic changes. How much a genetic change tells us about your chance of developing a disorder is not always clear. If your genetic results indicate that you have gene changes associated with an increased risk of heart disease, it does not mean that you definitely will develop heart disease. The opposite is also true. If your genetic results show that you do not have changes associated with an increased risk of heart disease, it is still possible that you develop heart disease. However for some rare diseases, people who have certain gene changes are guaranteed to develop the disease. When we are diagonosed out with certain genetic disease, are we suppose to disclose this result to our relatives? My answer is no.',
 "On one hand, we do not want this potential danger causing firghtenning affects in our families' later lives. When people around us know that we got certain disease, their altitudes will be easily changed, whether caring us too much or keeping away from us. And both are not what we want since most of us just want to live as normal people. Surrounded by such concerns, it is very likely that we are distracted to worry about these problems. It is a concern that will be with us during our whole life, because we will never know when the ''potential bomb'' will explode.",
 'On the other hand, if there are ways can help us to control or cure the disease, we can going through thses process from the scope of the whole family. For an example, if exercising is helpful for family potential disease, we can always look for more chances for the family to go exercise. And we keep track of all family members health conditions. At the same time, we are prepared to know when there are other members got this disease.',
 'Here I want to share Forest\'view on this issue. Although some people feel that an individual who is found to carry a dominant gene for Huntington\'s disease has an ethical obligation to disclose that fact to his or her siblings, there currently is no legal requirement to do so. In fact, requiring someone to communicate his or her own genetic risk to family members who are therefore also at risk is considered by many to be ethically dubious."',
 'Nothing is absolute right or wrong. If certain disease genetic test is very accurate and it is unavoidable and necessary to get treatment and known by others, it is OK to disclose the result. Above all, life is more important than secret.']

위와 같이 리스트 안에 각각의 문단을 요소로 입력하면, 인덱싱을 통해 원하는 문단에 쉽게 접근 가능합니다. 이제 mistake의 속성 정보를 활용해 문법 오류 위치를 확인해보겠습니다.

print(conllsoup.find('mistake'))
print('\n')
print('mistake:', paragraph_list[1][42:46])
print('\n')
print(paragraph_list[1][:98])
<mistake end_off="46" end_par="1" start_off="42" start_par="1">
<type>ArtOrDet</type>
<correction></correction>
</mistake>


mistake: more


What is genetic risk? Genetic risk refers more to your chance of inheriting a disorder or disease.

start_parend_par이 1이므로, paragraph_list에서 1번째 위치하는 문단을 가지고 와서 start_offend_off에 나와있는 42과 46번째 사이의 단어를 추출합니다. more이 문법 오류가 있는 단어이고, 해당 단어를 제거해야 한다고 나와 있습니다. 전체 문장을 확인해보니 more이 없어야 올바른 문장이 되는 것을 확인할 수 있습니다.

파이썬에서 string은 immutable합니다. 즉, 내용물을 수정할 수 없는 데이터 타입입니다. string을 수정하기 위해선 slicing방법을 사용해야 합니다. 문법 오류 단어가 존재하는 앞, 뒤 문장을 자른 후, 추후 올바른 단어와 병합을 하는 방식입니다.

paragraph_list[1][:42] + '[CORRECTION]' + paragraph_list[1][46:98]
'What is genetic risk? Genetic risk refers [CORRECTION] to your chance of inheriting a disorder or disease.'

위와 같이 전체 문단의 처음부터 start_off까지의 문자열과 end_off부터 마지막까지의 문자열 사이에 올바른 단어인 [CORRECTION]을 넣어주는 방법입니다. 실제 대체할 때는 [CORRECTION] 토큰 대신 올바른 단어를 입력해주면 됩니다. 98까지만 인덱싱한 이유는 문단 길이가 너무 길어서 출력 창 소비를 줄이기 위함입니다.

위와 같은 기능을 하는 edit_paragraph 사용자 정의 함수를 정의하겠습니다.

def edit_paragraph(paragraph, start, end, correction):
    return paragraph[:start] + correction + paragraph[end:]

그 다음으로 필요한 기능은 인덱스를 업데이트 해주는 기능입니다. 모든 mistake들은 원본 데이터를 기준으로 문자열의 좌표 위치가 기록돼 있으므로, 단어를 하나씩 대체할 때마다 그 이후에 오는 단어의 좌표를 업데이트 해줄 필요 있습니다. 예를 들어 I can do this all day라는 예시 문장이 있을 때 아래와 같이 수정사항이 표시돼있다고 가정하겠습니다.

  • [2, 5, ‘will’]

  • [18, 21, ‘night’]

즉 [2:5] 위치에 있는 단어를 ‘will’로 수정하고, [18:21] 위치에 있는 단어를 ‘day’로 수정한다는 뜻입니다. 앞서 정의한 edit_paragraph()함수를 활용한다면 아래와 같은 결과물이 나옵니다.

sample = 'i can do this all day'
print(sample, '\n')

print('mistake #1:', sample[2:5])

edit1 = edit_paragraph(sample, 2, 5, 'will')
print(edit1, '\n')

print('mistake #2:', sample[18:21])

edit2 = edit_paragraph(edit1, 18, 21, 'night')
print(edit2)
i can do this all day 

mistake #1: can
i will do this all day 

mistake #2: day
i will do this allnighty

첫번째 오류 단어인 can은 올바르게 will로 대체가 됐습니다. 하지만 두번째 오류 단어인 daynight으로 올바르게 대체가 되지 않았습니다. canwill로 바뀌면서 day의 인덱스가 [18:21]에서 [19:22]로 변경 됐기 때문입니다. 이처럼 단어를 하나씩 바꿀 때마다 그 이후에 오는 단어들의 인덱스가 변경되기 때문에, 인덱스를 업데이트 해줄 필요가 있습니다.

print('mistake #2:', sample[18:21])

edit2 = edit_paragraph(edit1, 19, 22, 'night')
print(edit2)
mistake #2: day
i will do this all night

지금껏 확인한 내용을 모두 정리하면 아래와 같은 사용자 정의 함수를 만들 수 있습니다.

import numpy as np

def edit_paragraph(paragraph, start, end, correction):
    return paragraph[:start] + correction + paragraph[end:]

def update_index(paragraph, offset, corrections):
    '''
    paragraph: 원문
    offset: 원본 위치정보
    corrections: 수정해야 하는 단어
    '''
    raw_words = []
    # 1. detect all the words at offset values in the paragraph / O(offset)
    for of in offset:
        raw_words.append(paragraph[of['start_off']:of['end_off']])

    # 2. compare length change between the detected words and corrections (map is faster than list comprehension when it doesn't use lambda) / O(offset)
    adjusted = np.array(list(map(len, corrections))) - np.array(list(map(len, raw_words)))

    # cumulate adjusted values / O(offset)
    for i in range(1, len(adjusted)):
        adjusted[i] += adjusted[i-1]
    
    # 3. adjust the index sequentially / O(offset)
    for i in range(1, len(offset)):
        offset[i]['start_off'] += adjusted[i-1]
        offset[i]['end_off'] += adjusted[i-1]

그리고 아래 알고리즘을 활용해 오류 단어들을 올바른 단어로 모두 수정해서 문법 오류가 없는 에세이를 생성하겠습니다.

all_docs = conllsoup.find_all('doc')
corrected_essays = []

for doc in all_docs:
    
    # extract paragraphs
    paragraph_lists = doc.find('text').text.strip().split('\n\n\n')

    # paragraph 별로 수정 진행
    # make list that contains offset values

    all_mistakes = doc.find_all('mistake')

    for para_idx, para in enumerate(paragraph_lists):
        edits = [mistake.attrs for mistake in all_mistakes if mistake.attrs['start_par'] == f'{para_idx}']

        if len(edits) == 0:
            continue
        
        edits = [{key: int(value) for key, value in edit.items()} for edit in edits]

        # make list that contains correction information 
        corrections = [mistake.find('correction').text for mistake in all_mistakes if mistake.attrs['start_par'] == f'{para_idx}'] #start par과 end par은 항상 같은가?
        
        # reindexing
        update_index(para, edits, corrections)

        for edit_idx, edit in enumerate(edits):
            para = edit_paragraph(para, int(edit['start_off']), int(edit['end_off']), corrections[edit_idx])

        paragraph_lists[para_idx] = para

    corrected_essays.append(' '.join(paragraph_lists))
len(corrected_essays)
50

총 50개의 에세이에 대해 전처리가 완료됐습니다. 첫번째 에세이 대해 수정 전/후를 비교하면 아래와 같습니다.

conllsoup.find('doc').find_all('mistake')[:3]
[<mistake end_off="46" end_par="1" start_off="42" start_par="1">
 <type>ArtOrDet</type>
 <correction></correction>
 </mistake>, <mistake end_off="125" end_par="1" start_off="118" start_par="1">
 <type>Nn</type>
 <correction>diseases</correction>
 </mistake>, <mistake end_off="627" end_par="1" start_off="620" start_par="1">
 <type>Trans</type>
 <correction>However,</correction>
 </mistake>]
conllsoup.find('text').text[:750] 
'\n\nKeeping the Secret of Genetic Testing\n\n\nWhat is genetic risk? Genetic risk refers more to your chance of inheriting a disorder or disease. People get certain disease because of genetic changes. How much a genetic change tells us about your chance of developing a disorder is not always clear. If your genetic results indicate that you have gene changes associated with an increased risk of heart disease, it does not mean that you definitely will develop heart disease. The opposite is also true. If your genetic results show that you do not have changes associated with an increased risk of heart disease, it is still possible that you develop heart disease. However for some rare diseases, people who have certain gene changes are guaranteed to d'
corrected_essays[0][:750] 
'Keeping the Secret of Genetic Testing What is genetic risk? Genetic risk refers  to your chance of inheriting a disorder or disease. People get certain diseases because of genetic changes. How much a genetic change tells us about your chance of developing a disorder is not always clear. If your genetic results indicate that you have gene changes associated with an increased risk of heart disease, it does not mean that you definitely will develop heart disease. The opposite is also true. If your genetic results show that you do not have changes associated with an increased risk of heart disease, it is still possible that you develop heart disease. However, for some rare diseases, people who have certain gene changes are guaranteed to develop'

출력 창 소비를 줄이기 위해 각 에세이 별 750번째 글자까지만 확인해보겠습니다. mistake태그에 있는 처음 3개의 라벨이 반영됬는지 살펴보겠습니다. 42번째와 46번째 글자 사이에 있는 more이 삭제가 되고, diseasediseases로 수정되고, HoweverHowever,로 반점이 추가된 것을 확인할 수 있습니다. 이처럼 edit_paragraph()update_index()를 통해 에세이에 있는 문법 오류를 교정할 수 있습니다.

edit_paragraph()함수에는 2개의 한계점이 있습니다. 단어를 삭제해야 하는 경우 단어 간 띄어쓰기 간격이 고르지 않게 수정되며, 대체 대상이 되는 단어의 위치 좌표가 띄어쓰기 까지 포함한 경우에도 수정 후 띄어쓰기 간격이 고르지 않습니다. 예를 들어 위치 좌표가 가르키는 대상이 ‘disease’가 아닌 ‘disease ‘로 되어 있다면 수정 후 띄어쓰기 간격이 고르지 않습니다.

Tip

현재까지 수정된 에세이를 pickle 파일로 저장하는 방법은 다음과 같습니다.

import pickle
file_name = "CoNLL14_corrected_essays.pkl"

open_file = open(file_name, "wb")
pickle.dump(corrected_essays, open_file)
open_file.close()

Tip

저장한 pickle파일은 아래 코드로 불러올 수 있습니다.

import pickle
file_name = "CoNLL14_corrected_essays.pkl"
open_file = open(file_name, "rb")
loaded_list = pickle.load(open_file)
open_file.close()

2.2 BEA-2019 Shared Task Dataset

BEA Workshop은 Association for Computational Linguistics Special Interest Group for Building Educational Applications (SIGEDU)에서 매년 운영하는 워크숍입니다. BEA Workshop은 Workshop on Innovative Use of NLP for Building Educational Applications의 약자이며, NLP를 교육 도메인에 적용하는 활용 사례들을 다루는 워크숍입니다.

2019년에 개최된 BEA-2019 워크숍에서 Shared Task로 GEC 문제를 다뤘습니다. 해당 대회에서 새롭게 제공한 데이터셋은 Cambridge English Write & Improve (W&I) 데이터와 LOCNESS corpus 입니다.

Write & Improve는 비영어권 학생들이 글쓰기 자료를 올려서 글의 품질을 향상할 수 있는 방법을 피드백 받는 온라인 플랫폼입니다. 2014년도 부터 W&I 데이터 어노테이터 (Annotator)들이 제출물들의 문법 오류를 수기로 기록했습니다. 본 대회에서는 해당 데이터를 훈련용, 검증용, 시험용 데이터로 공개했습니다.

그 외에도 LOCNESS corpus를 공개했습니다. LOCNESS는 영국과 미국의 원어민 학부생들이 작성한 약 400개의 에세이가 담긴 말뭉치입니다. 필터링 과정을 거친 총 100개의 에세이를 W&I 데이터 어노테이터들이 라벨링 해서 검증용, 시험용 데이터로 공개했습니다. 샘플 개수가 100개 밖에 되지 않아 별도로 훈련용 데이터로는 제공하지 않았습니다. 필터링 과정 및 추가적인 내용은 참고문헌 [2]에서 확인 가능합니다.

2.2.1 W&I + LOCNESS 데이터 탐색

먼저 wget명령어를 활용해 BEA 2019 Shared Task 홈페이지로 부터 압축 파일을 다운로드해 와서 bea19.tar.gz로 저장합니다.

!wget -O 'bea19.tar.gz' https://www.cl.cam.ac.uk/research/nl/bea2019st/data/wi+locness_v2.1.bea19.tar.gz
--2021-04-18 12:37:58--  https://www.cl.cam.ac.uk/research/nl/bea2019st/data/wi+locness_v2.1.bea19.tar.gz
Resolving www.cl.cam.ac.uk (www.cl.cam.ac.uk)... 128.232.0.20, 2a05:b400:110::80:14
Connecting to www.cl.cam.ac.uk (www.cl.cam.ac.uk)|128.232.0.20|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 6120469 (5.8M) [application/x-gzip]
Saving to: ‘bea19.tar.gz’

bea19.tar.gz        100%[===================>]   5.84M  6.90MB/s    in 0.8s    

2021-04-18 12:37:59 (6.90 MB/s) - ‘bea19.tar.gz’ saved [6120469/6120469]

  • 그림 2-3 bea19.tar.gz 불러온 후 파일 경로

정상적으로 파일이 불러왔다면 그림 2-3와 같이 bea19.tar.gz파일이 생성돼 있을 것입니다. 다음으로는 tar명령어를 활용해 압축을 해제하겠습니다.

!tar -xvf bea19.tar.gz
wi+locness/
wi+locness/json_to_m2.py
wi+locness/licence.wi.txt
wi+locness/readme.txt
wi+locness/license.locness.txt
wi+locness/json/
wi+locness/json/A.dev.json
wi+locness/json/A.train.json
wi+locness/json/B.dev.json
wi+locness/json/B.train.json
wi+locness/json/C.dev.json
wi+locness/json/C.train.json
wi+locness/json/N.dev.json
wi+locness/m2/
wi+locness/m2/ABCN.dev.gold.bea19.m2
wi+locness/m2/A.train.gold.bea19.m2
wi+locness/m2/A.dev.gold.bea19.m2
wi+locness/m2/B.train.gold.bea19.m2
wi+locness/m2/B.dev.gold.bea19.m2
wi+locness/m2/C.train.gold.bea19.m2
wi+locness/m2/C.dev.gold.bea19.m2
wi+locness/m2/N.dev.gold.bea19.m2
wi+locness/m2/ABC.train.gold.bea19.m2
wi+locness/test/
wi+locness/test/ABCN.test.bea19.orig
wi+locness/test/readme.txt

  • 그림 2-4 bea19.tar.gz 압축 해제 후 폴더 경로

압축을 해제하면 그림 2-4와같이 폴더가 구성돼있습니다. 각 폴더가 의미하는 바는 다음과 같습니다.

  • json: json 형태의 W&I 와 LOCNESS 파일

  • m2: m2 형태의 W&I 와 LOCNESS 파일

  • test: 테스트 데이터

W&I 파일은 CEFR (Common European Framework of Reference for Languages) 레벨에 따라 A, B, C등급으로 나눠져있습니다. A에서 C로 갈수록 높은 수준의 영어를 구사한다는 의미입니다. LOCNESS파일은 N으로 표시돼있습니다.

문제 생성시 활용할 지문으로는 C등급 파일과 원어민이 작성한 LOCNESS 파일의 에세이를 사용하도록 하겠습니다. 우선 C등급 파일의 경로를 path변수에 저장합니다.

path = 'wi+locness/json/C.dev.json'

그 후 open()함수로 파일을 열고나서 내용물을 readlines()를 통해 읽어서 data변수에 저장합니다.

with open(path) as f:
  data = f.readlines()

먼저 데이터 타입을 확인합니다. 데이터 타입이 list이므로 인덱싱이 가능합니다. 첫번째 요소에 접근해서 확인해보면 다음과 같은 문자열이 들어가 있는 것을 확인할 수 있습니다.

print(type(data))

print(type(data[0]))

print(data[0])
<class 'list'>
<class 'str'>
{"edits": [[0, [[89, 98, "saw"], [316, 316, " for"], [343, 343, " for"], [365, 365, " a"], [375, 375, " the"], [406, 406, " a"], [469, 471, "of"], [523, 527, "know"], [589, 594, "chips"]]]], "userid": "24796", "text": "Dear Mrs. Ashby, \n\nYesterday I was in Green Pepper Cafe for a meal with colleagues and I have seen the advertisement for a job at weekends in your cafe. \n\nI am very interested in this work and believe that my employment background is appropriate for it. \n\nI am a Tourism student and I need to work at weekends to pay my studies. \nI have worked one year in London as waiter in Hard Rock Cafe and 6 months as waiter also in Barcomi\u2019s Cafe in Berlin. So I have experience in service, costumer care and working long hours. \n\nI Know how to prepare different kinds of food: sandwiches, fish and fries, hamburgers, Italian pasta, etc \n\nI am also very good at dealing with people, I have never had a complaint sheet!\n\nI have total availability at weekends and also in summer. \n\nI speak Spanish, English and German. \n\nPlease, find attached a copy of my CV, which expands on my experience and achievements. \n\nI am looking forward to talking with you about the possibility of working in this position. I am available to do an interview when it is convenient for you.\n\nThank you for your time and consideration. \n\n\n\nYours Sincerely, \n\n\n\nMar\u00eda Luisa Castaneda del Acuna\n", "cefr": "C2.ii", "id": "1-169309"}

문자열을 살펴보면 key값과 value값으로 나눠 저장돼있는 딕셔너리 형태인것을 확인할 수 있습니다. 해당 데이터를 용이하게 처리하기 위해 문자열을 딕셔너리로 변환하겠습니다.

import json

data = list(map(json.loads, data))
print(type(data[0]))

print(data[0])
<class 'dict'>
{'edits': [[0, [[89, 98, 'saw'], [316, 316, ' for'], [343, 343, ' for'], [365, 365, ' a'], [375, 375, ' the'], [406, 406, ' a'], [469, 471, 'of'], [523, 527, 'know'], [589, 594, 'chips']]]], 'userid': '24796', 'text': 'Dear Mrs. Ashby, \n\nYesterday I was in Green Pepper Cafe for a meal with colleagues and I have seen the advertisement for a job at weekends in your cafe. \n\nI am very interested in this work and believe that my employment background is appropriate for it. \n\nI am a Tourism student and I need to work at weekends to pay my studies. \nI have worked one year in London as waiter in Hard Rock Cafe and 6 months as waiter also in Barcomi’s Cafe in Berlin. So I have experience in service, costumer care and working long hours. \n\nI Know how to prepare different kinds of food: sandwiches, fish and fries, hamburgers, Italian pasta, etc \n\nI am also very good at dealing with people, I have never had a complaint sheet!\n\nI have total availability at weekends and also in summer. \n\nI speak Spanish, English and German. \n\nPlease, find attached a copy of my CV, which expands on my experience and achievements. \n\nI am looking forward to talking with you about the possibility of working in this position. I am available to do an interview when it is convenient for you.\n\nThank you for your time and consideration. \n\n\n\nYours Sincerely, \n\n\n\nMaría Luisa Castaneda del Acuna\n', 'cefr': 'C2.ii', 'id': '1-169309'}

문자열이 모두 딕셔너리로 변환됐습니다. 다음으로는 딕셔너리의 키를 확인해보겠습니다.

data[0].keys()
dict_keys(['edits', 'userid', 'text', 'cefr', 'id'])

각 키의 의미는 다음과 같습니다.

  • cefr: text의 CEFR 레벨

  • edits: 문법 오류 위치와 올바른 단어

  • id: 에세이 고유번호

  • text: 에세이 원본

  • userid: 사용자 고유번호

BEA 2019 데이터도 2014 CoNLL 데이터 처럼 문법 오류의 위치가 인덱스로 기록돼있고, 어떤 단어로 고쳐야하는지 라벨링이 돼있습니다. edits 키를 확인해 어떻게 기록 돼있는지 확인해보겠습니다.

data[0]['edits']
[[0,
  [[89, 98, 'saw'],
   [316, 316, ' for'],
   [343, 343, ' for'],
   [365, 365, ' a'],
   [375, 375, ' the'],
   [406, 406, ' a'],
   [469, 471, 'of'],
   [523, 527, 'know'],
   [589, 594, 'chips']]]]

가장 첫번째 나오는 숫자 0은 어노테이터의 고유번호를 뜻합니다. 그 다음으로 나오는 정보들이 오류의 시작위치, 오류가 끝나는 위치, 그리고 올바른 단어를 의미합니다. 즉, 2014 CoNLL 전처리 시 활용한 함수들을 조금 수정하여 전처리에 활용가능합니다.

2.2.2 W&I + LOCNESS 데이터 전처리

전처리에 활용할 edit_paragraph()함수와 update_index_BEA()함수를 아래와 같이 정의합니다.

import numpy as np

def edit_paragraph(paragraph, start, end, correction):
    return paragraph[:start] + correction + paragraph[end:]

def update_index_BEA(paragraph, edits):
    '''
    paragraph: 원문
    edits: edits from BEA_2019
    '''
    raw_words = []
    corrections = []

    # 1. detect all the words at offset values in the paragraph / O(offset)
    for edit in edits:
        raw_words.append(paragraph[edit[0]:edit[1]])
        # make corrections
        corrections.append(edit[2])

    # 2. compare length change between the detected words and corrections (map is faster than list comprehension when it doesn't use lambda) / O(offset)
    adjusted = np.array(list(map(len, corrections))) - np.array(list(map(len, raw_words)))

    # cumulate adjusted values / O(offset)
    for i in range(1, len(adjusted)):
        adjusted[i] += adjusted[i-1]
    
    # 3. adjust the index sequentially / O(offset)
    for i in range(1, len(edits)):
        edits[i][0] += adjusted[i-1]
        edits[i][1] += adjusted[i-1]

    return corrections

BEA 2019에는 None Type이 존재하기도 하므로, 해당 샘플은 예외 처리 하는 로직이 필요합니다. 예를 들어 8번재 문서를 확인해보겠습니다.

edits = data[8]['edits'][0][1]

corrections = update_index_BEA(data[8]['text'], edits)

edits
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-40-65b5b20092fd> in <module>()
      1 edits = data[8]['edits'][0][1]
      2 
----> 3 corrections = update_index_BEA(data[8]['text'], edits)
      4 
      5 edits

<ipython-input-39-f8f89874637d> in update_index_BEA(paragraph, edits)
     19 
     20     # 2. compare length change between the detected words and corrections (map is faster than list comprehension when it doesn't use lambda) / O(offset)
---> 21     adjusted = np.array(list(map(len, corrections))) - np.array(list(map(len, raw_words)))
     22 
     23     # cumulate adjusted values / O(offset)

TypeError: object of type 'NoneType' has no len()

단어간 길이를 비교할 때 None Type이 있으면 에러가 나므로, 아래 코드 샘플을 활용해 None Type은 모두 제거하겠습니다.

[edit for edit in edits if edit[2] != None]

반복문을 통해 모든 에세이들에 대한 문법 교정을 실시하고, corrected_essays에 저장하겠습니다.

corrected_essays_WI = []

for doc in data:
    
    # extract paragraphs
    essay = doc['text']

    # paragraph 별로 수정 진행
    # make list that contains offset values

    edits = doc['edits'][0][1]
    edits = [edit for edit in edits if edit[2] != None] # NoneType 제거
    corrections = update_index_BEA(essay, edits)    

    for edit_idx, edit in enumerate(edits):
        essay = edit_paragraph(essay, edit[0], edit[1], corrections[edit_idx])

    corrected_essays_WI.append(' '.join(essay.split('\n\n\t')))
len(corrected_essays_WI)
70

위와 같은 과정을 LOCNESS 데이터에도 적용하겠습니다.

path = 'wi+locness/json/N.dev.json'
with open(path) as f:
    data = f.readlines()

data = list(map(json.loads, data))

corrected_essays_LOCNESS = []

for doc in data:
    
    # extract paragraphs
    essay = doc['text']

    # paragraph 별로 수정 진행
    # make list that contains offset values

    edits = doc['edits'][0][1]
    edits = [edit for edit in edits if edit[2] != None] # NoneType 제거
    corrections = update_index_BEA(essay, edits)    

    for edit_idx, edit in enumerate(edits):
        essay = edit_paragraph(essay, edit[0], edit[1], corrections[edit_idx])

    corrected_essays_LOCNESS.append(' '.join(essay.split('\n\n\t')))
len(corrected_essays_LOCNESS)
50

CoNLL 데이터셋에서 문법 교정한 50개의 에세이, W&I 데이터셋에서 문법 교정한 70개의 에세이, 그리고 LOCNESS 데이터셋에서 문법 교정한 50개의 에세이를 모두 합쳐 하나의 리스트로 만든 후, pickle 파일로 저장하겠습니다.

total_essays = corrected_essays + corrected_essays_WI + corrected_essays_LOCNESS
len(total_essays)
170
import pickle

file_name = "CoNLL+BEA_corrected_essays.pkl"
open_file = open(file_name, "wb")
pickle.dump(total_essays, open_file)
open_file.close()

지금까지 에세이 데이터인 CoNLL 14 데이터와 W&I + LOCNESS 데이터를 탐색해보고 전처리를 진행해봤습니다. 전처리한 데이터를 활용해 3장과 4장에서 에세이에 걸맞는 질문을 자동으로 생성해보겠습니다.

참고문헌

[1] Ng, H. T., Wu, S. M., Briscoe, T., Hadiwinoto, C., Susanto, R. H., & Bryant, C. (2014). The CoNLL-2014 Shared Task on Grammatical Error Correction. Proceedings of the Eighteenth Conference on Computational Natural Language Learning: Shared Task, 1–14. https://doi.org/10.3115/v1/W14-1701

[2] Bryant, C., Felice, M., Andersen, I. E., & Briscoe, T. (2019). The BEA-2019 Shared Task on Grammatical Error Correction. Proceedings of the Fourteenth Workshop on Innovative Use of NLP for Building Educational Applications, 52–75. https://doi.org/10.18653/v1/w19-4406

[3] L. (2018, March 21). Difference Between HTML and SGML. Compare the Difference Between Similar Terms. https://www.differencebetween.com/difference-between-html-and-vs-sgml/

[4] Saive, R. (2020, July 3). 18 Tar Command Examples in Linux. TecMint. https://www.tecmint.com/18-tar-command-examples-in-linux/