HAZEL

[ NLP : CH4. 전처리 ] 전처리, 정제, 분절, 병렬 코퍼스 정렬,서브워드 분절(BPE알고리즘), SentencePiece,분절 복원 ( detokenization ), Torchtext 본문

DATA ANALYSIS/NLP

[ NLP : CH4. 전처리 ] 전처리, 정제, 분절, 병렬 코퍼스 정렬,서브워드 분절(BPE알고리즘), SentencePiece,분절 복원 ( detokenization ), Torchtext

Rmsid01 2020. 12. 25. 23:38

4장. 전처리 

4-1. 전처리

4.1.1. 코퍼스 ( 말뭉치 ) 

: 여러 단어들로 이루어진 문장

 

* 코퍼스의 종류

- 단일언어 코퍼스 : 한가지 언어로 구성된 코퍼스

- 이중 언어 코퍼스 : 두개의 언어로 구성된 코퍼스

- 다중 언어 코퍼스 : 두개 이상의 언어로 구성된 코퍼스

- 병령 코퍼스 : 언어간의 쌍으로 구성된 코퍼스 ( 영문과 한글이 함께 짝을 이루는 데이터 )

 

4.1.2. 전처리 과정 

: 코퍼스 수집 -> 정제 -> 문장 단위 분절 -> 분절 -> 병렬 코퍼스 정렬 ( 생략 가능 ) -> 서브워드 분절 

 

 


4-2. 정제

4.2.1. 전각 문자 제거 

: 전각문자를 반각문자로 변환해주는 작업 필요

전각문자 : 특수문자 기호. 문자의 폭이 일반적인 영문자의 고정폭의 두배정도의 폭을 가지는 문자. 

반각문자 : 우리가 상요하는 기호 . 전각문자의 폭의 절반을 폭으로 하는 문자 

 

4.2.2. 대소문자 통일

: 같은 의미를 가지는 단어를 하나의 형태로 통일시켜 희소성을 줄이는 효과를 기대할 수 있다.

그러나, 딥러닝의 단어 임베딩을 통해 다양한 단어들을 비슷한 값의 벡터로 나타낼 수 있게 되어, 대소문자 통일과 같은 문제 해결의 필요성이 감소되었다.

 

4.2.3. 정규식 표현을 사용하여 정제

: ROW 데이터의 경우 , 데이터에 노이즈가 많기 때문에 정규표현식 ( import re ) 을 하여, 데이터를 깔끔하게 해준다.

 


4-3. 문장단위 분절

문장 단위로 분절하기 위해 '.'로 구분해서 나누어줄 수 는 있지만, 그러면 U.S. 와 같은 약자를 마주칠때, 출력값이 이상하게 될 수 있다. 따라서 1) 직접 분절하는 모듈을 만들기 2) NLTK의 자연어 처리 툴킷을 사용하는 것의 방법이 있다.

 

4.3.1. 문장 단위 분절

import sys, fileinput ,re
from nltk.tokenize import sent_tokenize
import nltk
nltk.download('punkt')
  
import sys, fileinput, re
from nltk.tokenize import sent_tokenize

if __name__ == "__main__":
    for line in fileinput.input("input.txt",openhook=fileinput.hook_encoded("utf-8")):
        if line.strip() != "":   # 이 if문은 지금은 필요 x 
            line = re.sub(r'([a-z])\.([A-Z])', r'\1. \2', line.strip())

            sentences = sent_tokenize(line.strip())  # 이 함수를 사용하면, 문장단위로 나누어주고 리스트 형식으로 만들어 줌 
            print(sentences, '\n')
            
            for s in sentences:
                if s != "":
#                     sys.stdout.write(s + "\n")
                    print(s) # 둘다 가능하다 
                    
fileinput.close() # fileinput을 열어두면, 다시 실행할 때 에러가 발생해서 닫아줌

위의 코드를 입력하면, 문장 단위로 분절되는 것을 알 수 있다. 

아래가 위의 코드의 결과물이다. [ 글의 출처  : 카카오 라이언의 이야기..  ]

for line in fileinput.input("input.txt",openhook=fileinput.hook_encoded("utf-8")):
    sentences = sent_tokenize(line.strip())  
    for i in sentences:
        print(i)

이렇게만 사용해도, 위의 코드와 같은 결과를 얻어 낼 수는 있다. 


4-4 분절 ( Tokenization )

: 띄어쓰기 정규화, 접사를 어근에서 분리하는 역할을 통해 희소성 문제를 해소한다. 

 

* 많이 사용 되는 분절 프로그램

- 한국어 : Mecab / KoNLPy

- 일본어 : Mecab

- 중국어 : Stanford Parser / PKU Paser / Jieba

 

4.4.1. 한국어 분절  : konlpy.org/ko/latest/ 참고

- 기본적으로 어근과 접사를 분리하는 역할을 하며, 나아가 잘못된 띄어쓰기에 대해 올바르게 교정하는 역할을 함.

from konlpy.tag import Kkma
kkma = Kkma()

line = '둥둥섬의 왕위 계승자로 태어난 수사자 라이언. 무뚝뚝한 표정과는 다르게 배려심이 많고 따뜻한 리더십을 가지고 있습니다.'
tokenized = kkma.pos(line)
print(tokenized)

- KoNLPy 패키지에는 아래의 5가지가 있다. 

    • Hannanum Class
    • Kkma Class
    • Komoran Class
    • Mecab Class
    • Okt Class

4.4.2. 영어 분절 

: 영어의 경우, 기본적으로 띄어쓰기가 잘 통일되어있어, 이부분에는 큰 이슈가 없다. 다만, 쉼표, 마침표, 인용부호 등을 띄어주어야한다. 

 


4-5. 병렬 코퍼스 정렬

: “ 병렬 코퍼스는 여러 문장 단위로 정렬됨

 

보통 크롤링을 해서 얻은 문서들은 문서와 문서와의 맵핑일 뿐 문장대 문장에 관한 정렬을 이루어지지 않음

따라서, 각 문장에 대해 정렬해줘야함 , 병렬 코퍼스 제작을 해줘야함.

 

4.5.1. 병렬 코퍼스 제작 프로세스 

4.5.2. 사전 생성

: 기존에 사전이 없으면 사전을 만들어야 한다.

* 페이스북 MUSE : 이 만든 사전을 구축하는 방법과 코드를 제공함 .

  - 병렬 코퍼스가 없는 상황에도 수행할 수 있기 때문에, '비지도 학습 '이라고 할 수 있음. 

 

4.5.3. CTK를 활용한 정렬

: 이중언어 코퍼스의 문장 정렬을 수행하는 오픈소스

CTK의 결과

1) 일대일 맵핑  2) 일대다 맵핑  3) 다대일 맵핑

 


4.6. 서브워드 분절

: BPE 알고리즘을 이용한 서브워드 단위 분절은 필수 전처리 방법이다.

 

* 서브워드 분절 : 단어는 의미를 가진 더 작은 서브워드들의 조합으로 이루어진다는 가정하에 적용되는 알고리즘

 

* 서브워드 분절로 얻을 수 있는 효과

1) 희소성 감소

2) UNK ( unkown ) 토큰에 대한 효율적인 대처 가능 => 자연어 처리 알고리즘의 결과물 품질 향상  

   : 자연어는 입력으로 받을 때, 단어들의 시퀀스로 받아드리는데 이때 UNK가 등장하면 모델의 확률이 크게 감소한다. 또한, 적절한 문장의 임베딩 또는 생성이 어렵다.  

 

  *  OOV(Out-Of-Vocabulary) == UNK ( unkown Token )

 

4.6.1 BPE 알고리즘에 대해서 공부해보자! 

BPE ( 바이트 페어 인코딩 : Byte Pair Encoding )

: 데이터 압축 알고리즘 -> 자연어처러리의 서브워드 분리 알고리즘으로 응용됨 

 

1) BPE ( 바이트 페어 인코딩 : Byte Pair Encoding ) 의 작동 방법
- 연속적으로 가장 많이 등장하는 글자의 쌍을 찾아서 하나의 글자로 병합하는 방식을 수행

'가나나다다다다가나나다다'

# 가장 많이 등장하는 글자는 '다다'  -> A로 치환

'가나나AA가나나A'

# 그 다음으로 많이 등장하는 글자 '가나나'  -> B 치환
'BAABA'

# 이제 더이상 병합할 쌍이 없으므로 종료됨

 

2) 자연어 처리에서 BPE

  • 빈도수에 기반하여 가장 많이 등장한 쌍을 병합하는 것
  • 자연어 처리에서의 BPE는 단어를 분리한다는 의미임. 즉, 글자 단위에서 점차적으로 단어 집합을 만들어 내는 Bottom up 방식의 접근을 사용함
  • https://arxiv.org/pdf/1508.07909.pdf  : BPE 논문 
  • BPE의 특징은 알고리즘의 동작을 몇 회 반복(iteration)할 것인지를 사용자가 정한다는 점입니다. 여기서는 총 10회를 수행한다고 가정합니다. 다시 말해 가장 빈도수가 높은 유니그램의 쌍을 하나의 유니그램으로 통합하는 과정을 총 10회 반복합니다. 위의 딕셔너리에 따르면 빈도수가 현재 가장 높은 유니그램의 쌍은 (e, s)입니다.
import re, collections
from IPython.display import display, Markdown, Latex

num_merges = 4

dictionary = {'w a l k </w>' : 5,
         'w a l k e d </w>' : 2,
         'd r k i n g </w>':6,
         'e a t i n g </w>':3
         }

def get_stats(dictionary):
    # 유니그램의 pair들의 빈도수를 카운트
    pairs = collections.defaultdict(int)
    for word, freq in dictionary.items():
        symbols = word.split()
        for i in range(len(symbols)-1):
            pairs[symbols[i],symbols[i+1]] += freq
    print('현재 pair들의 빈도수 :', dict(pairs))
    return pairs

def merge_dictionary(pair, v_in):
    v_out = {}
    bigram = re.escape(' '.join(pair))
    p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
    for word in v_in:
        w_out = p.sub(''.join(pair), word)
        v_out[w_out] = v_in[word]
    return v_out

bpe_codes = {}
bpe_codes_reverse = {}

for i in range(num_merges):
    display(Markdown("### Iteration {}".format(i + 1)))
    pairs = get_stats(dictionary)
    best = max(pairs, key=pairs.get)
    dictionary = merge_dictionary(best, dictionary)

    bpe_codes[best] = i
    bpe_codes_reverse[best[0] + best[1]] = best

    print("new merge: {}".format(best))
    print("dictionary: {}".format(dictionary))

위 코드 결과 : KING 과 TING 이 생겼다.

이 결과에 의거해서, 새로운 단어가 나타났을 때, 분리하는 일에 참고할 수 있다.

 

3) BPE를 참고하여 만들어진 모델

- Wordpiece Model :  병합 되었을 때 코퍼스의 우도를 가장 높이는 쌍을 병합

- Unigram Language Model Tokenizer : 각각 서브워드들에 대해서 손실을 계산함. 즉, 서브워드가 단어 집합에서 제거 되었을 경우, 코퍼스 우도가 감소하는 정도를 말함. 

 

4.6.2. SentencePiece

 : BPE 알고리즘을 실무에서 사용하기 위한 패키지로 만든 것 

1 ] 학습시키기

* 학습 파라미터는 아래 눌러서 확인 

더보기

* input : 학습시킬 파일 (텍스트파일만) 
* model_prefix : 만들어질 모델 이름 마음대로 커스텀 가능   
* vocab_size : 단어 집합의 크기 
    * 커질수록 성능이 좋아지고 모델 파라미터수가 증가함--->시간이오래걸릴듯함 
* model_type : 사용할 모델 (unigram(default), bpe, char, word) 
* max_sentence_length: 문장의 최대 길이 
* pad_id, pad_piece: pad token id, 값 
* unk_id, unk_piece: unknown token id, 값 
* bos_id, bos_piece: begin of sentence token id, 값 
* eos_id, eos_piece: end of sequence token id, 값 
* user_defined_symbols: 사용자 정의 토큰 내가 원하는 형태소만 가져오기 

# template을 Train시켜준다.
spm.SentencePieceTrainer.Train(template)

학습이 완료되면, 두개의 파일이 생성된다.

1) subword_tokenizer_kor.vocab ( subword_tokenizer_kor 은 내가 이름을 지정해준것 ) 

: vocab 파일에서 학습된 서브워드들을 확인 할 수 있다.

# 학습이 완료되면, subword_tokenizer_kor.model 과 subword_tokenizer_kor.vocab 이 생성됨 
# vocab 파일에서 학습된 서브워드들을 확인 할 수 있다.

with open('subword_tokenizer_kor.vocab', encoding = 'utf-8') as f :
    vo = [ doc.strip().split('\t') for doc in f]
# v[0] : token name
# v[1] : token score 

word2idx = { w[0] : i for i, w in enumerate(vo)}

2) subword_tokenizer_kor.model : 생성된 model파일을 로드해서 단어 시퀀스를 정수 시퀀스로 바꾸는 인코딩 작업이나  반대로 변환하는 디코딩 작업을 할 수 있음. 

model_file = 'subword_tokenizer_kor.model'
model = spm.SentencePieceProcessor()
model.load(model_file)

* # 기능이 tensorflow 에도 있다.  위의 코드와 같은 기능을 하는 코드이다.

import tensorflow as tf

serialized_model_proto = tf.io.gfile.GFile(vocab_file, 'rb').read()

sp = spm.SentencePieceProcessor()
sp.load_from_serialized_proto(serialized_model_proto)

 

2 ] 테스트 하기

* 만들어진 모델의 함수는 아래 눌러서 확인 

더보기

* GetPieceSize() : 단어 집합의 크기를 확인합니다. = vocab_size
* encode_as_pieces : 문장을 입력하면 서브 워드 시퀀스로 변환합니다.
* encode_as_ids : 문장을 입력하면 정수 시퀀스로 변환합니다.
* idToPiece : 정수로부터 맵핑되는 서브 워드로 변환합니다.
* PieceToId : 서브워드로부터 맵핑되는 정수로 변환합니다.
* DecodeIds : 정수 시퀀스로부터 문장으로 변환합니다.
* DecodePieces : 서브워드 시퀀스로부터 문장으로 변환합니다.
* encode : 문장으로부터 인자값에 따라서 정수 시퀀스 또는 서브워드 시퀀스로 변환 가능합니다.

tmp = '여행같은 음악 지친 하루 끝 힐링이 필요한 당신에게 추천하는 인디곡'

print('단어집합의 크기                              : ' , model.GetPieceSize( )) # 학습시킬때 단어 량을 vocab_size = 32000 로 설정 해줘서 3200 이나옴 
print("정수(1785)로부터 맵핑되는 서브워드 변환      : " , model.IdToPiece(1785)) 
print("서브워드(▁힐링이)로부터 맵핑되는 정수로 변환 : ", model.PieceToId("▁힐링이") , '\n')

# 서브워드 시퀀스로부터 문장을 반환
print(model.DecodeIds([360, 175, 18, 539, 252, 446, 1785, 814, 1122, 990, 3787]), '\n') 

# 문장으로부터 인자값에 다라 정수 시퀀스 또는 서브워드 시퀀스로 변환
print(model.encode("힐링이 필요한 당신에게 추천하는 인디곡", out_type = str))
print(model.encode("힐링이 필요한 당신에게 추천하는 인디곡", out_type = int), '\n')

print(model.encode_as_pieces( tmp )) # 문장을 입력하면 서브 워드 시퀀스로 변환
print( model.encode_as_ids( tmp )) # 문장을 입력하면 정수 시퀀스로 변환 

 


4.7. 분절 복원 ( detokenization )

: 전처리 과정에서 분절을 수행하고 다시 분절 복원 ( detokenization )을 하는 과정 

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')

 


4.8. 토치 텍스트

: 자연어 처리 문제 또는 텍스트에 관한 머신러닝/딥러닝을 수행하는 데이터를 읽고 전처리하는 코드를 모아둔 라이브러리

 

4.8.1. 자연어처리 분야에서 주로 쓰이는 학습 데이터 형태

4.8.2. Torchtext

Field class :  우리가 읽고자 하는 텍스트 파일 내의 필드를 정의 ( 보통 tab 을 기준으로 함 )

Dataset class : 정의된 각 필드를 읽어들임

 

이터레이터 : 읽어들인 코퍼스는 미리 주어진 미니배치 크기에 따라서 나뉠 수 있게 함

패딩(PAD : Padding) : 미니배치 내에 문장의 길이가 다를 경우에는 필요에 따라 문장의 앞 뒤에 패딩을 삽입함

                    - BOS , EOS 와 함께 하나의 단어 또는 토큰과 같이 취급을 받음

=> 훈련코퍼스에 대해 어휘사전을 만들어 각 단어(토큰)을 숫자로 맵핑하는 작업을 수행


** 본 게시글은 자연어 책을 공부하면서 정리한 것 

출처 : 김기현의 자연어 처리 딥러닝 캠프 _ 파이 토치 편

wikidocs.net/22592

ratsgo.github.io/statistics/2017/09/22/information/ 

konlpy.org/ko/latest/