해당 포스팅은 AIFFEL에서 제공한 학습자료를 통해 공부한 것을 정리한 것임을 밝힙니다.
준비물
KoNLPy의 Mecab 클래스를 활용하여 실습하고자 한다.
from konlpy.tag import Mecab
mecab = Mecab()
print(mecab.morphs('자연어처리가너무재밌어서밥먹는것도가끔까먹어요'))
['자연어', '처리', '가', '너무', '재밌', '어서', '밥', '먹', '는', '것', '도', '가끔', '까먹', '어요']
1. 데이터 다운로드 및 분석
import os
import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np
%matplotlib inline
학습환경을 구성하고 데이터를 다운로드 받는다.
데이터는 한국어의 형태소 분석과 품사 태깅, 기계 번역 연구를 위해 공개된 데이터이다.
이번 실습에서 사용할 데이터는 한국어-영어 병렬을 이루는 말뭉치 중 한국어 부분이다.
import os
path_to_file = os.getenv('HOME')+'/aiffel/sp_tokenizer/data/korean-english-park.train.ko'
with open(path_to_file, "r") as f:
raw = f.read().splitlines()
print("Data Size:", len(raw)) # 문장의 개수
print("Example:")
for sen in raw[0:100][::20]: print(">>", sen)
Data Size: 94123
Example:
>> 개인용 컴퓨터 사용의 상당 부분은 "이것보다 뛰어날 수 있느냐?"
>> 북한의 핵무기 계획을 포기하도록 하려는 압력이 거세지고 있는 가운데, 일본과 북한의 외교관들이 외교 관계를 정상화하려는 회담을 재개했다.
>> "경호 로보트가 침입자나 화재를 탐지하기 위해서 개인적으로, 그리고 전문적으로 사용되고 있습니다."
>> 수자원부 당국은 논란이 되고 있고, 막대한 비용이 드는 이 사업에 대해 내년에 건설을 시작할 계획이다.
>> 또한 근력 운동은 활발하게 걷는 것이나 최소한 20분 동안 뛰는 것과 같은 유산소 활동에서 얻는 운동 효과를 심장과 폐에 주지 않기 때문에, 연구학자들은 근력 운동이 심장에 큰 영향을 미치는지 여부에 대해 논쟁을 해왔다.
각 문장의 길이를 시각화하여 확인해보자.
확인 후에 데이터를 얼마나 사용할 지 타협점을 정의해볼 수 있다.
min_len = 999
max_len = 0
sum_len = 0
for sen in raw:
length = len(sen)
if min_len > length: min_len = length
if max_len < length: max_len = length
sum_len += length
print("문장의 최단 길이:", min_len)
print("문장의 최장 길이:", max_len)
print("문장의 평균 길이:", sum_len // len(raw))
sentence_length = np.zeros((max_len), dtype=np.int)
for sen in raw:
sentence_length[len(sen)-1] += 1
plt.bar(range(max_len), sentence_length, width=1.0)
plt.title("Sentence Length Distribution")
plt.show()
문장의 최단 길이: 1
문장의 최장 길이: 377
문장의 평균 길이: 60
위의 그래프를 통해 확인해봐야 할 사항 3가지가 있다.
첫 번째, 길이가 1인 문장은 어떻게 생긴 문장일까?
두 번째, 그래프 앞 쪽에 치솟은 구간에 어떤 데이터가 담겨있을까?
세 번째, 사용할 데이터 구간을 어떻게 설정을 해야 할까?
하나씩 확인해보자.
1) 길이가 1인 문장은 어떻게 생긴 문장일까?
def check_sentence_with_length(raw, length):
count = 0
for sen in raw:
if len(sen) == length:
print(sen)
count += 1
if count > 100: return
check_sentence_with_length(raw, 1)
'
길이가 1인 문장은 '인 노이즈 데이터임을 확인할 수 있었다.
2) 그래프 앞 쪽에 치솟은 구간에 어떤 데이터가 담겨있을까?
for idx, _sum in enumerate(sentence_length):
# 문장의 수가 1500을 초과하는 문장 길이를 추출합니다.
if _sum > 1500:
print("Outlier Index:", idx+1)
Outlier Index: 11
Outlier Index: 19
Outlier Index: 21
인덱스 11의 데이터를 확인해보자.
check_sentence_with_length(raw, 11)
...
폭탄테러가 공포 유발
그는 "잘 모르겠다.
그는 "잘 모르겠다.
그는 "잘 모르겠다.
그는 "잘 모르겠다.
그는 "잘 모르겠다.
그는 "잘 모르겠다.
그는 "잘 모르겠다.
그는 "잘 모르겠다.
그는 "잘 모르겠다.
케냐 야생동물 고아원
경유 1200원대로…
더 내려야 하는 이유
케냐 야생동물 고아원
경유 1200원대로…
더 내려야 하는 이유
케냐 야생동물 고아원
경유 1200원대로…
...
중복에 대한 처리가 제대로 되지 않은 것을 확인할 수 있다. 그래서 set을 활용하여 중복을 제거하고자 한다.
중복을 제거한 후, 분포를 다시 확인해보자.
min_len = 999
max_len = 0
sum_len = 0
cleaned_corpus = list(set(raw)) # set를 사용해서 중복을 제거합니다.
print("Data Size:", len(cleaned_corpus))
for sen in cleaned_corpus:
length = len(sen)
if min_len > length: min_len = length
if max_len < length: max_len = length
sum_len += length
print("문장의 최단 길이:", min_len)
print("문장의 최장 길이:", max_len)
print("문장의 평균 길이:", sum_len // len(cleaned_corpus))
sentence_length = np.zeros((max_len), dtype=np.int)
for sen in cleaned_corpus: # 중복이 제거된 코퍼스 기준
sentence_length[len(sen)-1] += 1
plt.bar(range(max_len), sentence_length, width=1.0)
plt.title("Sentence Length Distribution")
plt.show()
Data Size: 77591
문장의 최단 길이: 1
문장의 최장 길이: 377
문장의 평균 길이: 64
깔끔하게 데이터 처리가 된 것을 확인할 수 있다. 데이터 개수도 17,000개 가량 줄었다.
3) 사용할 데이터 구간을 어떻게 설정을 해야 할까?
이후에 미니 배치를 만들 것을 생각하면 모든 데이터를 다 사용하는 것은 연산 측면에서 비효율적이다.
미니 배치 특성 상 각 데이터의 크기가 모두 동일해야 하기 때문에 가장 긴 데이터를 기준으로 padding처리를 해야한다.
길이 별로 정렬하여 미니 배치를 구성해 Padding을 최소화하는 방법도 있지만, 이것은 데이터를 섞는 데 편향성이 생길 수 있으므로 지양해야 한다.
이번 실습에서는 길이 150 이상의 데이터를 제거하고 사용하고자 한다. 그리고 너무 짧은 데이터는 오히려 노이즈로 작용할 수 있으니 길이가 10미만인 데이터도 제거한다.
max_len = 150
min_len = 10
# 길이 조건에 맞는 문장만 선택합니다.
filtered_corpus = [s for s in cleaned_corpus if (len(s) < max_len) & (len(s) >= min_len)]
# 분포도를 다시 그려봅니다.
sentence_length = np.zeros((max_len), dtype=np.int)
for sen in filtered_corpus:
sentence_length[len(sen)-1] += 1
plt.bar(range(max_len), sentence_length, width=1.0)
plt.title("Sentence Length Distribution")
plt.show()
2. 공백 기반 토큰화
정제된 데이터를 공백 기반으로 토큰화하여 list에저장한 후, 아래 tokenize() 함수를 사용해 단어사전과 Tensor 데이터를 얻고 단어 사전의 크기를 확인해보자.
def tokenize(corpus): # corpus: Tokenized Sentence's List
tokenizer = tf.keras.preprocessing.text.Tokenizer(filters='')
tokenizer.fit_on_texts(corpus)
tensor = tokenizer.texts_to_sequences(corpus)
tensor = tf.keras.preprocessing.sequence.pad_sequences(tensor, padding='post')
return tensor, tokenizer
정제된 데이터를 공백 기반으로 토큰화하여 저장해보자.
split_corpus = []
for kor in filtered_corpus: # 정제된 데이터에서 문장을 하나씩 가져옴
split_corpus.append(kor.split()) # 가져온 문장을 split 한 후 split_corpus에 추가
공백 기반 토큰화 후의 단어사전 길이를 확인해보자.
split_tensor, split_tokenizer = tokenize(split_corpus) # 위에서 정의한 tokenize() 사용
print("Split Vocab Size:", len(split_tokenizer.index_word))
Split Vocab Size: 237435
생성된 단어사전을 확인해보자.
for idx, word in enumerate(split_tokenizer.word_index):
print(idx, ":", word)
if idx > 10: break
0 : 이
1 : 밝혔다.
2 : 있다.
3 : 말했다.
4 : 수
5 : 있는
6 : 그는
7 : 대한
8 : 위해
9 : 전했다.
10 : 지난
11 : 이번
동사로 이루어진 단어를 살펴보면 공백 기반 토큰화의 문제점을 확인할 수 있다.
1번 단어인 '밝혔다'는 '밝히다', '밝다' 등 유사한 의미를 지니고 있음에도 전혀 다른 단어로 분류된다. 이 때문에 공백 기반 토큰화는 불필요하게 큰 단어사전을 가지게 되어 연산량이 증가된다.
3. 형태소 기반 토큰화
한국어 형태소 분석기는 대표적으로 Khaiii와 KoNLPy가 사용된다. 이번 프로젝트에서는 KoNLPy의 MeCab 클래스를 활용한다.
def mecab_split(sentence):
return mecab.morphs(sentence) # mecab.morphs()를 사용해서 형태소분석을 합니다.
mecab_corpus = []
for kor in filtered_corpus: # 정제된 데이터에서 문장을 하나씩 가져옴
mecab_corpus.append(mecab_split(kor)) # 가져온 문장을 split 한 후 mecab_corpus에 추가
형태소 기반 토큰화를 진행한 후의 단어사전 길이를 확인해보자.
mecab_tensor, mecab_tokenizer = tokenize(mecab_corpus)
print("MeCab Vocab Size:", len(mecab_tokenizer.index_word))
MeCab Vocab Size: 52279
공백 기반 단어사전에 비해 단어 수가 현저히 줄어든 것을 확인할 수 있다.
연산량이 감소하여 더 빠른 학습이 가능하고 모델이 튜닝해야 하는 매개변수(parameter)가 줄어들어 학습도 잘 된다. 그렇기에 한국어를 처리할 때는 공백 기반 토큰화는 지양해야 한다.
자주 사용하는 SentencePiece 같은 Subword 기반 토큰화보다 형태소 분석기가 좋은 성능을 내는 사례가 있는데, 그 중 대표적인 사례는 KorBERT이다.
공공 인공지능 오픈 API·DATA 서비스 포털
과학기술정보통신부의 R&D 과제를 통해 개발한 다양한 인공지능 기술 및 데이터를 누구나 사용할 수 있도록 제공
aiopen.etri.re.kr
자연어 처리에서 토크나이저가 성능에 미치는 영향도가 크지만, 아래의 내용들을 생각해볼 필요가 있다.
구글에서 배포한 BERT 모델은 한국어 전용 코퍼스를 바탕으로 훈련된 것이 아니라 Multilingual 코퍼스를 바탕으로 훈련된 것이다. Word Piece 모델 안에 포함된 subword 안에도 한국어가 아닌 여러 언어의 것이 섞여 있어서 한국어 자연어처리 태스크에 특화된 모델이 아니다.
엑소브레인의 모델은 한국어 코퍼스에 특화된 형태로 언어모델과 토크나이저가 훈련된 것이다. 그러므로 엑스브레인과 구글의 BERT 모델의 한국어 태스크 성능 차이는 한국어에 특화된 언어 모델을 구축했을 때 기대할 수 있는 성능 향상치로 해석할 수 있다.
WordPiece 모델은 해당 언어의 문법적 및 의미적 사전 정보가 반영되지 않은 채 순수하게 통계적인 빈도 기반으로 자주 사용되는 반복 패턴을 사전으로 등재해 놓은 것에 불과하다.
그에 비해 정확한 한국어 문법과 의미 정보를 바탕으로 개발된 형태소 분석기가 정확하게 동작한다면, 현재까지 가장 성능이 좋다고 알려진 Subword 기반의 토크나이저보다 더 성능이 좋을 수 있음을 보여준다.
언어는 지속적으로 변하기에 정교한 형태소 분석기의 성능을 유지하기 위해서는 지속적인 데이터 관리와 유지보수 작업이 필요하다.
SentencePiece 모델은 코퍼스 데이터로부터 쉽게 추출해서 생성이 가능하고 Subword 기반이기 때문에 새롭게 생성되는 단어에 대한 OOV(Out-of-Vocabulary) 문제에 대해서도 robust하게 대처할 수 있다. 또한 언어는 중립적이기에 여러 언어가 섞여 나오는 텍스트를 처리하는데 능하다.
특정 언어에 대한 부가지식이 없어도 엔지니어가 그 언어에 대한 작업을 손쉽게 진행할 수 있도록 해준다. 그 언어에 특화된 토크나이저의 성능에 뒤지지 않거나 대체로 능가하는 성능을 보여준다.
지금까지 문장을 Tensor로 Encoding하는 과정을 배웠다. 이제는 모델이 생성한 Tensor를 문장으로 Decoding하는 과정을 진행해보자.
- Case 1 : tokenizer.sequences_to_texts() 함수를 사용하여 Decoding
# Case 1
texts = mecab_tokenizer.sequences_to_texts([mecab_tensor[100]])
print(texts[0])
“ 주요 업종 의 2006 상반기 실적 및 하반기 전망 조사 ” 에서 , 섬유 업종 은 저 가 중국 산 제품 의 국내외 시장 잠식 이 이어질 것 이 며 , 고유 가 와 환율 하락 은 이미 줄어든 유통 마진 의 감소 를 더욱 부채 질 할 것 이 라고 대한 상공 회의소 는 밝혔 다 .
- Case 2 : tokenizer.index_word 를 사용하여 Decoding
# Case 2
sentence = ""
for w in mecab_tensor[100]:
if w == 0: continue
sentence += mecab_tokenizer.index_word[w] + ""
print(sentence)
“주요업종의2006상반기실적및하반기전망조사”에서,섬유업종은저가중국산제품의국내외시장잠식이이어질것이며,고유가와환율하락은이미줄어든유통마진의감소를더욱부채질할것이라고대한상공회의소는밝혔다.
'AIFFEL > Going Deeper(NLP)' 카테고리의 다른 글
[Going Deeper(NLP)] 7. Seq2seq와 Attention (0) | 2022.03.30 |
---|---|
[Going Deeper(NLP)] 6. 임베딩 내 편향성 알아보기 (0) | 2022.03.29 |
[Going Deeper(NLP)] 5. 워드 임베딩 (0) | 2022.03.28 |
[Going Deeper(NLP)] 3. 텍스트의 분포로 벡터화 하기 (0) | 2022.03.23 |
[Going Deeper(NLP)] 1. 텍스트 데이터 다루기 (0) | 2022.03.15 |