0.1 corpus corpus는 말뭉치라고도 불리며 보통 여러단어들로 이루어진 문장을 의미. train data를 위해 이런 다수의 문장으로 구성된 corpus가 필요하다.
∙ monolingual corpus: 한 가지 언어로 구성된 corpus ∙ bilingual corpus: 두 개의 언어로 구성된 corpus ∙ multilingual corpus: 더 많은 수의 언어로 구성된 corpus ∙ parallel corpus: 언어간의 쌍(ex.영문-한글)으로 구성된 corpus
0.2 preprocessing 과정 개요 ① corpus 수집 ② Normalize ③ 문장단위 Tokenize ④ Tokenize ⑤ parallel corpus 정렬 ⑥ subword tokenize
1. Corpus 수집
corpus를 수집하는 방법은 여러가지가 있는데, 무작정 웹사이트에서 corpus 크롤링을 하면 법적 문제가 될 수 있다.
저작권은 물론, 불필요한 트래픽이 웹서버에 가중되는 과정에서 문제발생가능
따라서 적절한 웹사이트에서 올바른 방법 or 상업적 목적이 아닌 경우로 제한된 크롤링이 권장된다.
해당 웹사이트의 크롤링 허용여부는 사이트의 robots.txt를 보면 된다.
ex) TED의 robot.txt 확인방법
1.1 monolingual corpus 수집 가장 쉽게 구할 수 있는 corpus(인터넷에 그냥 널려있기 때문)이기에 대량의 corpus를 손쉽게 얻을 수 있다. 다만, 올바른 도메인의 corpus를 수집, 사용가능한 형태로 가공하는 과정이 필요하다.
1.2 multilingual corpus 수집 자동 기계번역을 위한 parallel corpus를 구하기는 monolingual corpus에 비해 상당히 어렵고 'he'와 'she'와 같은 대명사가 사람이름 등의 고유명사로 표현될 때가 많아 이런 번역에 대한 문제점들을 두루 고려해야한다.
2. Normalization
2.1 전각/반각 문자 제거 대부분의 일본/중국어 문서와 일부 한국어문서의 숫자, 영자, 기호가 전각문자일 가능성이 존재하기에 이를 반각 문자로 변환해주는 작업이 필요하다.
2.2 대소문자 통일 다양한 표현의 일원화는 하나의 의미를 갖는 여러 단어를 하나의 형태로 통일해 희소성(sparsity)을 줄여준다. 다만, 딥러닝이 발전하면서 다양한 단어들을 비슷한 값의 vector로 단어임베딩으로 대소문자 해결의 필요성이 줄어들었다.
2.3 정규 표현식을 사용한 Normalize 크롤링으로 얻은 다량의 corpus의 경우, 특수문자 및 기호 등에 의한 Noise가 존재한다. 또한 웹사이트의 성격에 따른 일정 패턴을 갖는 경우도 있기에 효율적으로 noise를 감지, 없애야 하는데, 이때 인덱스의 사용이 필수적이다. 아래는 정규식시각화사이트(https://regexper.com/)를 활용해 나타낸 것이다.
[ ] 사용 2 or 3 or 4 or 5 or c or d or e와 같은 의미를 갖는다. 면 아래의 좌측과 같다. ex) [2345cde]
- 사용 연속된 숫자나 알파벳 등을 표현할 수 있다. ex) [2-5c-e]
^ 사용 not을 기호 ^을 사용해 표현할 수 있다. ex) [2-5c-e]
( ) 사용 괄호를 이용해 group 생성할 수 있다. ex) (x)(yz)
? + * 사용 ?: 앞의 수식하는 부분이 나타나지 않거나 한번만 나타날 때 +: 앞의 수식하는 부분이 한번 이상 나타날 경우 *: 앞의 수식하는 부분이 나타나지 않거나 여러번 나타날 경우
ex) x? ex) x+ ex) x*
^와 $의 사용 [ ] 내에 포함되지 않을 때, ^ 은 라인의 시작을 의미 $ 은 라인의 종료를 의미 ex) ^x$
예제 1)
아래의 개인정보(전화번호)가 포함된 corpus를 dataset으로 사용할 때, 개인정보를 제외하고 사용하고자 한다면?
단, 항상 마지막 줄에 전화번호 정보가 있는 것은 아니라면?
Hello Kim, I would like to introduce regular expression in this section
~~
Thank you!
Sincerely,
Kim: +82-10-1234-5678
개인정보의 규칙을 먼저 파악해보자. 국가번호는 최대 3자리이고 앞에 +가 붙을 수 있으며 전화번호 사이에 -가 들어갈 수도 있다.
전화번호는 빈칸 없이 표현되며 지역번호가 들어갈 수 있고 마지막은 항상 4자리 숫자이다 등등...
import re
regex = r"([\w]+\s*:?\s*)?\(?\+?([0-9]{1,3})?\-[0-9]{2,3}(\)|\-)?[0-9]{3,4}\-?[0-9]{4}"
x = "Name - Kim: +82-10-9425-4869"
re.sub(regex, "REMOVED", x)
출력: Name - REMOVED
예제 2) 치환자 사용
아래의 예제에서 알파벳 사이에 있는 숫자를 제거해야한다면?
만약 단순히 [0-9]+ 로 숫자를 찾아 없앤다면 숫자만 있거나 숫자가 가장자리에 있는 경우도 사라지게 된다.
x = '''abcdefg
12345
ab12
12ab
a1bc2d
a1
1a'''
따라서 괄호로 group을 생성하고 바뀔 문자열 내에서 역슬래시(\)와 함께 숫자를 이용해 마치 변수명처럼 가리킬 수 있다.
regex = r'([a-z])[0-9]+([a-z])'
to = r'1\2\'
y = '\n'.join([re.sub(regex, to, x_i) for x_i in x.split('\n')])
([a-z])[0-9]+([a-z])
3. 문장단위 tokenization
다만, 보통 다루려는 문제들은 입력단위가 아닌, 문장단위인 경우가 많고
대부분의 경우, 한 라인에 한 문장만 있어야 한다.
따라서 여러 문장이 한 라인에 있거나 한 문장이 여러 라인에 걸쳐있다면, 이를 분절(tokenize)해줘야 한다.
이를 위해 직접 분절하는 알고리즘을 만들기보다는 널리 알려진 NLP Toolkit, NLTK에서 제공하는 sent_tokenize 이용이 주가 된다.
물론, 추가적인 전처리 및 후처리가 필요한 경우들도 존재한다.
3.1 sentence tokenization 예제
import sys, fileinput, re
from nltk.tokenize import sent_tokenize
if __name__ == "__main__":
for line in fileinput.input():
if line.strip() != "":
line = re.sub(r'([a-z])\.([A-Z])', r'\1. \2', line.strip())
sentences = sent_tokenize(line.strip())
for s in sentences:
if s != "":
sys.stdout.write(s + "\n")
3.2 문장 합치기 및 분절 예제
import sys, fileinput
from nltk.tokenize import sent_tokenize
if __name__ == "__main__":
buf = []
for line in fileinput.input():
if line.strip() != "":
buf += [line.strip()]
sentences = sent_tokenize(" ".join(buf))
if len(sentences) > 1:
buf = sentences[1:]
sys.stdout.write(sentences[0] + '\n')
sys.stdout.write(" ".join(buf) + "\n")
4. Tokenization
풀고자 하는 문제, 언어에 따라 형태소 분석, 단순한 분절을 통한 정규화를 수행하는데, 특히 띄어쓰기에 관해 살펴보자.
한국어의 경우, 표준화 과정이 충분하지 않아 띄어쓰기가 지멋대로인 경우가 상당히 많으며, 띄어쓰기가 문장해석에 큰 영향을 주지 않아 이런 현상이 더욱 더 가중되는 경향이 존재한다. 따라서 한국어의 경우, 정규화를 해줄 때, 표준화된 띄어쓰기를 적용하는 과정도 필요하며 교착어로써 접사를 어근에서 분리해주는 역할도 필요하기에 희소성문제를 해소하기도 한다. 이런 한국어의 tokenization을 위한 프로그램으로는 C++로 제작된 Mecab, Python으로 제작된 KoNLPy가 전처리를 수행한다. https://github.com/kh-kim/nlp_with_pytorch_examples/blob/master/chapter-04/tokenization.ipynb
영어의 경우, 기본적으로 띄어쓰기가 있고, 기본적으로 대부분의 경우 규칙을 매우 잘 따르고 있다. 영어의 경우, 보통 앞서 언급했듯 기본적인 띄어쓰기가 잘 통일되어 있는 편이므로 띄어쓰기 자체에는 큰 정규화 문제가 존재하지 않는다. 다만 쉼표(comma), 마침표(period), 인용부호(quotation) 등을 띄어주어야 하므로 Python으로 제작된 NLTK를 이용한 전처리를 수행한다.
$ pip install nltk==3.2.5
일본어와 중국어의 경우, 모든 문장이 띄어쓰기가 없는 형태를 하고 있지만, 적절한 언어모델구성을 위해 띄어쓰기가 필요하다. 일본어의 경우, C++ base의 Mecab을, 중국어의 경우 Java base의 Stanford Parser, PKU Parser를 사용한다.
5. 병렬 Corpus 정렬
예를들어 영어신문과 한글신문이 맵핑되는, 문서와 문서단위 맵핑의 경우,
문장 대 문장에 관한 정렬은 이루어져 있지 않았기에 일부 불필요한 문장들을 걸러내야한다.
5.1 parallel corpus 제작과정 개요 ①source와 target 언어간의 단어사전을 준비, 준비된 단어사전이 있으면 ⑥으로 이동, 없다면 아래과정을 따른다.
5.2 단어사전 생성 facebook의 MUSE는 parallel corpus가 없는 상황에서 사전을 구축하는 방법과 코드를 제공한다.
MUSE는 각 monolingual corpus를 통해 구축된 언어별 word embedding vector에 대해 다른 언어의 embedding vector와 mapping시켜 단어 간의 번역을 수행할 수 있는, unsupervised learning이다. <>를 구분문자(delimeter)로 사용해 한 라인에 source단어와 target단어를 표현한다.
ex) MUSE를 통해 unsupervised learning을 사용해 결과물로 얻은 영한 단어 번역사전의 일부. 상당히 정확한 단어간 번역을 볼 수 있다.
stories <> 이야기
stories <> 소설
contact <> 연락
contact <> 연락처
contact <> 접촉
green <> 초록색
green <> 빨간색
dark <> 어둠
dark <> 짙
5.3 CTK를 활용한 정렬 앞서 구성한 사전은 CTK의 입력으로 사용되는데, CTK는 이 사전을 바탕으로 parallel의 문장정렬을 수행한다. CTK는 bilingual corpus의 문장정렬을 수행하는 오픈소스로 Perl을 사용해 구현되었다.
기존 혹은 자동으로 구축된 단어사전을 참고해 CTK는 문장정렬을 수행하는데, 여러 라인으로 구성된 언어별 문서에 대해 문장 정렬한 결과의 예제는 아래와 같다.
위의 예시처럼 어떤 문장들은 버려지기도 하고 일대일(one-to-one)맵핑, 일대다(one-to-many), 다대일(many-to-one)맵핑이 이뤄지기도 한다.
ex) CTK를 쉽게 사용하기 위해 파이썬으로 감싼 스크립트 예제 이때, CTK_ROOT에 CTK 위치를 지정해 사용할 수 있다.
import sys, argparse, os
BIN = "NEED TO BE CHANGED"
CMD = "%s -c %f -d %s %s %s %s"
OMIT = "omitted"
DIR_PATH = './tmp/'
INTERMEDIATE_FN = DIR_PATH + "tmp.txt"
def read_alignment(fn):
aligns = []
f = open(fn, 'r')
for line in f:
if line.strip() != "":
srcs, tgts = line.strip().split(' <=> ')
if srcs == OMIT:
srcs = []
else:
srcs = list(map(int, srcs.split(',')))
if tgts == OMIT:
tgts = []
else:
tgts = list(map(int, tgts.split(',')))
aligns += [(srcs, tgts)]
f.close()
return aligns
def get_aligned_corpus(src_fn, tgt_fn, aligns):
f_src = open(src_fn, 'r')
f_tgt = open(tgt_fn, 'r')
for align in aligns:
srcs, tgts = align
src_buf, tgt_buf = [], []
for src in srcs:
src_buf += [f_src.readline().strip()]
for tgt in tgts:
tgt_buf += [f_tgt.readline().strip()]
if len(src_buf) > 0 and len(tgt_buf) > 0:
sys.stdout.write("%s\t%s\n" % (" ".join(src_buf), " ".join(tgt_buf)))
f_tgt.close()
f_src.close()
def parse_argument():
p = argparse.ArgumentParser()
p.add_argument('--src', required = True)
p.add_argument('--tgt', required = True)
p.add_argument('--src_ref', default = None)
p.add_argument('--tgt_ref', default = None)
p.add_argument('--dict', required = True)
p.add_argument('--ratio', type = float, default = 1.1966)
config = p.parse_args()
return config
if __name__ == "__main__":
assert BIN != "NEED TO BE CHANGED"
if not os.path.exists(DIR_PATH):
os.mkdir(DIR_PATH)
config = parse_argument()
if config.src_ref is None:
config.src_ref = config.src
if config.tgt_ref is None:
config.tgt_ref = config.tgt
cmd = CMD % (BIN, config.ratio, config.dict, config.src_ref, config.tgt_ref, INTERMEDIATE_FN)
os.system(cmd)
aligns = read_alignment(INTERMEDIATE_FN)
get_aligned_corpus(config.src, config.tgt, aligns)
6. Subword Tokenization (with Byte Pair Encoding)
6.1 BPE (Byte Pair Algorithm) BPE를 통한 subword tokenization은 현재 가장 필수적인 전처리방법이다. ex) concentrate = con(together) + centr(=center) + ate(=make) ex) 집중(集中) = 集(모을 집) + 中(가운데 중) [subword tokenization 알고리즘] - 단어는 의미를 갖는 더 작은 subwords의 조합으로 이뤄진다는 가정하에 - 적절한 subword를 발견해 해당 단위로 쪼개어 - 어휘 수를 줄이고 sparsity를 효과적으로 줄이는 방법. - 특히 UNK(Unknown) Token에 대해 효율적 대처이다.
[UNK Token , OOV. &. BPE] ∙ U.K[Unknown Token]: train corpus에 없는 단어 ∙ OOV[Out-of-Vocabulary] 문제: U.K로 인해 문제를 푸는 것이 까다로워지는 현상.
자연어처리에서 문장을 입력으로 받을 때 단순히 단어들의 시퀀스로 받기에 UNK Token은 자연어처리 모델의 확률을 망가뜨리고 적절한 embedding(encoding) 또는 생성이 어려워지는 지뢰이다. 특히, 문장생성의 경우, 이전단어를 기반으로 다음 단어를 예측하기에 더욱 어려워진다. subword 분리로 OOV문제를 완화하는데, 가장 대표적인 subword 분리알고리즘이 바로 BPE(Byte Pair Encoding) 이다.
하지만 subword단위의 tokenization을 진행하는 BPE 알고리즘의 경우, 신조어나 오타(typo)같은 UNK Token에 대해 subword 단위나 문자(character)단위로 쪼개 기존 train data의 token들의 조합으로 만들어버릴 수 있다. 즉, UNK 자체를 없앰으로써 효율적으로 UNK에 대처하여 성능을 향상시킬 수 있다.
ex)
[영어 NLTK]에 의해 분절된 원문
Natural language processing is one of biggest streams in A.I
[영어 BPE]로 subword로 분절된 원문
_Natural _language _processing _is _one _of _biggest _stream s _in _A. I
7. Detokenization
전처리과정에서 tokenization을 수행하였으면, 다시 detokenization을 수행해줘야한다.
즉, 아래와 같은 전처리과정을 따른다.
∙ 언어별 tokenizer모듈(ex. NLTK)로 분절 수행 이때, 새롭게 분절되는 공백과의 구분을 위해 기존 공백에 _ 기호를 삽입
∙ subword단위 tokenization(ex. BPE알고리즘)을 수행 이때, 이전 과정까지의 공백과 subword단위 분절로 인한 공백 구분을 위해 특수문자 _ 기호를 삽입 즉, 기존 공백의 경우 _ _를 단어 앞에 갖게 되는 것.
∙ Detokenize 진행 먼저 공백을 제거한다. 이후 (_를 2개 갖는)_ _ 문자열을 공백으로 치환한다. 마지막으로 _ 를 제거한다.
7.1 tokenization 후처리 Detokenization을 쉽게하기 위해 tokenize 이후 특수문자를 분절과정에서 새롭게 생긴 공백 다음에 삽입해야한다.
예제) 기존 공백과 전처리단계에서 생성된 공백을 서로 구분하기 위한 특수문자 _ 를 삽입하는 코드
tokenizer.py
import sys
STR = '▁'
if __name__ == "__main__":
ref_fn = sys.argv[1]
f = open(ref_fn, 'r')
for ref in f:
ref_tokens = ref.strip().split(' ')
input_line = sys.stdin.readline().strip()
if input_line != "":
tokens = input_line.split(' ')
idx = 0
buf = []
# We assume that stdin has more tokens than reference input.
for ref_token in ref_tokens:
tmp_buf = []
while idx < len(tokens):
if tokens[idx].strip() == '':
idx += 1
continue
tmp_buf += [tokens[idx]]
idx += 1
if ''.join(tmp_buf) == ref_token:
break
if len(tmp_buf) > 0:
buf += [STR + tmp_buf[0].strip()] + tmp_buf[1:]
sys.stdout.write(' '.join(buf) + '\n')
else:
sys.stdout.write('\n')
f.close()
7.2 Detokenize 예제 위의 스크립트(tokenizer.py)를 사용하는 방법은 아래와 같다. 주로 다른 분절모듈의 수행 후에 pipe를 사용해 붙여서 사용한다.
import sys
if __name__ == "__main__":
for line in sys.stdin:
if line.strip() != "":
if '▁▁' in line:
line = line.strip().replace(' ', '').replace('▁▁', ' ').replace('▁', '').strip()
else:
line = line.strip().replace(' ', '').replace('▁', ' ').strip()
sys.stdout.write(line + '\n')
else:
sys.stdout.write('\n')
8. Torchtext 라이브러리
8.1 Torchtext 란? Torchtext 라이브러리는 자연어처리를 수행하는 data를 읽고 전처리하는 코드를 모아둔 라이브러리이다. (https://pytorch.org/text/stable/index.html) Torchtext라이브러리를 활용하면 쉽게 text파일을 읽어내 훈련에 사용할 수 있다.