ML&DL
임베딩 - FastText (한글 자소 분리)
joyHong
2020. 8. 16. 23:26
한글로된 코퍼스를 자소 분리하여 FastText의 입력으로 전달함으로
각각의 하나의 문자에 대하여 n-gram을 하도록 할 수도 있다.
이와 같이 사용하려면
학습 전 준비단계에서 해야 하는 일 한가지와
테스트시 해야 하는 일 두가지만 추가하면 된다.
1. (준비단계) 자소분리된 코퍼스 준비
2. (테스트) 테스트를 위한 문자를 자소로 분리하여 전달
3. (테스트) 결과로 나온 단어들의 자소를 합쳐 원래 단어로 변경
그럼 자소로 분리하는 것부터 하도록 한다.
코드
import util.utils as util
from tqdm import tqdm
def process_jamo(tokenized_corpus_fname, output_fname):
toatal_lines = sum(1 for line in open(tokenized_corpus_fname, 'r', encoding='utf-8'))
with open(tokenized_corpus_fname, 'r', encoding='utf-8') as f1, \
open(output_fname, 'w', encoding='utf-8') as f2:
for _, line in tqdm(enumerate(f1), total=toatal_lines):
sentence = line.replace('\n', '').strip()
processed_sentence = util.jamo_sentence(sentence)
f2.writelines(processed_sentence + '\n')
tokenized_corpus_fname = 'D:/Data/embedding/data/tokenized/corpus_mecab.txt'
output_fname = 'D:/Data/embedding/data/tokenized/corpus_mecab_jamo.txt'
process_jamo(tokenized_corpus_fname, output_fname)
utils.py 코드
import re
from soynlp.hangle import compose, decompose, character_is_korean
doublespace_pattern = re.compile('\s+')
def jamo_sentence(sent):
def transform(char):
if char == ' ':
return char
cjj = decompose(char)
if len(cjj) == 1:
return cjj
cjj_ = ''.join(c if c != ' ' else '-' for c in cjj)
return cjj_
sent_ = []
for char in sent:
if character_is_korean(char):
sent_.append(transform(char))
else:
sent_.append(char)
sent_ = doublespace_pattern.sub(' ', ''.join(sent_))
return sent_
def jamo_to_word(jamo):
jamo_list, idx = [], 0
while idx < len(jamo):
if not character_is_korean(jamo[idx]):
jamo_list.append(jamo[idx])
idx += 1
else:
jamo_list.append(jamo[idx:idx + 3])
idx += 3
word = ""
for jamo_char in jamo_list:
if len(jamo_char) == 1:
word += jamo_char
elif jamo_char[2] == "-":
word += compose(jamo_char[0], jamo_char[1], " ")
else:
word += compose(jamo_char[0], jamo_char[1], jamo_char[2])
return word
(** utils.py의 내용은 https://github.com/ratsgo/embedding/blob/master/preprocess/unsupervised_nlputils.py 의 내용을 참조하였습니다.)
토큰단위로 분리된 코퍼스를 입력으로 받아 각각의 단어를 자소 분리하여 새로운 코퍼스를 만드는게 주 작업 내용이다.
자소 분리된 코퍼스가 준비되었으면 fasttext 스킵그램 모델을 학습을 실행한다.
코드
from gensim.models import FastText
from tqdm import tqdm
import logging
corpus_fname = 'D:/Data/embedding/data/tokenized/corpus_mecab_jamo.txt'
model_fname = 'D:/Data/embedding/data/word-embeddings/fasttext_jamo/fasttext'
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
print('corpus 생성')
corpus = [sent.strip().split(" ") for sent in tqdm(open(corpus_fname, 'r', encoding='utf-8').readlines())]
print("학습 중")
model = FastText(corpus, size=100, workers=4, sg=1, iter=2, word_ngrams=5)
model.save(model_fname)
print(f"학습 소요 시간 : {model.total_train_time}")
# https://projector.tensorflow.org/ 에서 시각화 하기 위해 따로 저장
model.wv.save_word2vec_format(model_fname + "_vis")
print('완료')
학습이 완료되면 자소 분리된 단어들로 임베딩된 결과들 생성된다.
이를 확인하기 위해서는 이전 블로그 내용인 fasttext의 코드를 조금 수정하여
입력 단어를 자소분리하여 입력하고
결과를 자소 결합하도록 변경한다.
코드
from gensim.models import FastText
import util.utils as util
def transform(list):
return [(util.jamo_to_word(w), r) for (w, r) in list]
# 모델을 로딩하여 가장 유사한 단어를 출력
loaded_model = FastText.load("D:/Data/embedding/data/word-embeddings/fasttext_jamo/fasttext")
print(loaded_model.wv.vectors.shape)
print(transform(loaded_model.wv.most_similar(util.jamo_sentence('최민식'), topn=5)))
print(transform(loaded_model.wv.most_similar(util.jamo_sentence('남대문'), topn=5)))
print(transform(loaded_model.wv.most_similar(util.jamo_sentence('남대몬'), topn=5)))
결과
(235753, 100)
[('최민수', 0.9500991702079773), ('조민식', 0.9426417350769043), ('최민섭', 0.9422751069068909), ('최민기', 0.9422407150268555), ('최민호', 0.9389006495475769)]
[('남대문로', 0.9196799993515015), ('남대문시장', 0.9048603773117065), ('동대문', 0.9022918939590454), ('서대문', 0.8923516869544983), ('사대문', 0.8766767978668213)]
[('남대구', 0.8452219367027283), ('남대문', 0.8340914845466614), ('남대성', 0.8294790387153625), ('남대문시장', 0.8286895751953125), ('강남대역', 0.8146793842315674)]
자소를 분리하여 임베딩할 경우 장점도 있겠고 그에 따른 단점도 있어 보인다.
판단은 각자가 해야 하지 않을까 싶다..