본문 바로가기

AI/NLP Study

[NLP] 단어사전 만들기

이 전 글에서 자연어 전처리 방법 중 토큰화에 대해 알아보고 관련된 모델을 공부했습니다.

 

이번에는 여러 한국어 형태소 분석기를 직접 사용해보고 그 결과를 비교해보겠습니다.

 

먼저 실습을 진행할 Google Colab의 환경 설정을 해줍니다.

# Mecab 설치

!apt-get update
!apt-get install g++ openjdk-8-jdk
!pip install konlpy JPype1-py3
!bash <(curl -s https://raw.githubusercontent.com/konlpy/konlpy/master/scripts/mecab.sh)
!pip install --upgrade gensim

사용할 데이터는 한국어 형태소 분석과 품사 태깅, 기계 번역 연구를 위해 공개된 데이터입니다. 

공개된 데이터 중 한국어-영어 병렬을 이루는 말뭉치 중에서 한국어 부분을 사용하겠습니다.

다른 데이터는 https://github.com/jungyeul/korean-parallel-corpora 이 주소로 가시면 볼 수 있습니다.

 

그럼 먼저 데이터를 불러오겠습니다.

file_path = '/content/drive/MyDrive/AIFFEL/GoingDeeper(NLP)/2_단어사전만들기/korean-english-park.train.ko'

with open(file_path, "r") as f:
    raw = f.read().splitlines()

print("Data Size:", len(raw))

print("Example:")
for sen in raw[0:100][::20]: print(">>", sen)

문장은 총 94123개가 포함되어 있습니다. 각 문장이 어느 정도 길이를 가지는지 확인해보고 지나치게 긴 문장을 삭제하거나 짧은 데이터를 확인해보겠습니다. 짧더라도 단어라면 번역을 학습할 수 있기 때문에 한 번 확인을 해보는 게 좋을 것 같습니다.

 

문장의 최단 길이, 최장 길이, 평균 길이를 구한 후 문장 길이 분포를 막대그래프로 확인해보겠습니다.

import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt

%matplotlib inline

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. 길이 1짜리 문장은 무엇인지
  2. 그림에서 치솟는 임의의 구간은 무엇인지
  3. 어디서부터 어디까지 잘라서 써야 하는지

의문점에 대해 하나하나 살펴보겠습니다.

# 길이가 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

# 길이가 1인 문장 확인
check_sentence_with_length(raw, 1)

# output
'

이번에는 문장의 개수가 1500개를 초과하는 문장 길이를 추출하겠습니다.

for idx, _sum in enumerate(sentence_length):
  if _sum > 1500:
    print('Outlier Index : ', idx+1)

check_sentence_with_length(raw, 11)

중복된 문장조차 제거되지 않은 것을 볼 수 있습니다. 중복 치를 제거하기 위해 Python의 set을 이용하겠습니다. 

하지만 set을 사용하면 순서에 대한 정보가 사라지기 때문에 만약 순서가 중요한 데이터를 다룬다면 다른 방법으로 중복 치를 제거해야 합니다.

 

set을 이용해 중복 치를 제거한 후 분포를 다시 확인해보겠습니다.

min_len = 999
max_len = 0
sum_len = 0

# 중복치 제거 부분
cleaned_corpus = list(set(raw))
print('Data Size : ', len(cleaned_corpus))

for sen in cleaned_corpus:
  length = len(sen)
  if length < min_len:
    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()

처음에 비해 그래프가 깔끔해졌습니다. 

이제 여기선 모든 데이터를 사용할지 선택해야 합니다.

데이터를 모델에 넣는다고 하면 가장 긴 데이터를 기준으로 Padding처리를 해주게 되는데 이때 너무 길이가 길면 문제가 생길 수 있기 때문에 길이 150이 넘는 데이터는 제거하고 사용하겠습니다. 그리고 너무 길이가 짧은 10 미만의 데이터도 삭제하겠습니다.

max_len = 150
min_len = 10

filtered_corpus = [s for s in cleaned_corpus if (len(s) < max_len) and (len(s) >= min_len)]

sentenced_length = np.zeros((max_len), dtype=np.int)

for sen if 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()

이제 데이터의 분포를 보면 어느 정도 nice한 모양을 띄고 있습니다.

이제 이 데이터를 공백 기반으로 토큰 화하여 list에 저장한 후, tokenize() 함수를 사용해 단어 사전과 Tensor데이터를 얻고, 단어 사전의 크기를 확인해보겠습니다.

 

먼저 tokenize() 함수를 정의하겠습니다.

def tokenize(corpus):
  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, paddind='post')
  
  return tensor, tokenizer

이제 문장을 공백을 기준으로 나눠서 split_corpus 리스트에 넣어준 뒤, 공백 기반 토큰화 진행 후 단어 사전의 길이를 확인해보겠습니다.

그리고 단어 사전이 잘 만들어졌는지 샘플을 뽑아 확인해보겠습니다.

# 공백 기준으로 문장 나누기
split_corpus = []

for kor in filtered_corpus:
  split_corpus.append(kor.split())
 
split_tensor, split_tokenizer = tokenize(split_corpus)

print('Split Vocab Size : ', len(split_tokenizer.index_word))

# output
Split Vocab Size : 237435


# 생성된 단어 사전 샘플 확인

for idx, word in enumerate(split_tokenizer.word_index):
  print(idx, ':', word)
  
  if idx > 10:
    break

뽑은 샘플로 확인해보면 공백 기반 토큰화의 문제점을 확인할 수 있습니다. 단어 사전 1번의 '밝혔다'는 '밝히다', '밝다' 등과 비슷한 의미지만 사전에서 전혀 다른 단어로 분류되어 있습니다. 이처럼 공백 기반 토큰화는 불필요하게 큰 단어 사전을 가지게 되어 연산량을 증가시킵니다.

이러한 문제를 해결하기 위해 형태소 분석기가 존재합니다.

 

한국어 형태소 분석기는 대표적으로 khaiii와 KoNLPy가 사용됩니다. 이번에는 KoNLPy의 MeCab클래스를 이용하겠습니다.

tokenizer = Mecab()

def mecab_split(sentence):
  tokenized_sentence = tokenizer.morphs(sentence)
  
  return tokenized_sentence
  
mecab_corpus=[]

for kor in filtered_corpus:
  mecab_corpus.append(mecab_split(kor))
  

mecab_tensor, mecab_tokenizer = tokenize(mecab_corpus)

print("MeCab Vocab Size : ", len(mecab_tokenizer.word_index))

# output
MeCab Vocab Size : 52279

단어 사전의 크기를 보면 공백 기반 토큰화로 만들어진 단어 사전에 비해 현저히 줄어든 것을 볼 수 있습니다. 

 

이 외에도 한국어 형태소 분석기로 유명한 KorBERT 모델이 있습니다.

이 모델의 구조는 https://aiopen.etri.re.kr/service_dataset.php 여기서 자세히 볼 수 있습니다.

 

이번에는 tensor로 encoding 된 문장을 다시 decoding 해보겠습니다.

 

먼저 만들어진 tensor가 어떤 모양인지 확인해줍니다.

decoding을 두 가지 방법을 진행해보겠습니다.

  1. tokenizer.sequences_to_texts() 이용
  2. tokenizer.index_word 이용

SentencePiece 

이번에는 형태소 분석기가 아닌 Google에서 제공하는 오픈소스 기반 Sentence Tokenizer/Detokenizer인 SentencePiece모델을 사용해보겠습니다. SentencePiece모델은 BPE와 unigram 2가지 subword 토크나이징 모델 중 하나를 선택해 사용할 수 있도록 패키징 한 모델로, 아래 링크의 페이지에서 자세한 내용을 볼 수 있습니다.

 

GitHub - google/sentencepiece: Unsupervised text tokenizer for Neural Network-based text generation.

Unsupervised text tokenizer for Neural Network-based text generation. - GitHub - google/sentencepiece: Unsupervised text tokenizer for Neural Network-based text generation.

github.com

 

그럼 구글 코랩(google colab)에서 SentencePiece모델을 한 번 사용해보겠습니다.

먼저 모델을 설치해주고 학습해줍니다.

!pip install sentencepiece

import sentencepiece as spm
temp_file_path = '.../korean_english_park.train.ko.temp'

vocab_size = 8000

with open(temp_file_path, 'w') as f:
  for row in filtered_corpus:
    f.write(str(row) + '\n')

spm.SentencePieceTrainer.Train(
    '--input={} --model_prefix=korean_spm --vocab_size={} --model_tyype=unigram'.format(temp_file_path, vocab_size)
)

!ls -l korean_spm*

학습이 완료된 다음 model, vocab 파일이 생성된 것을 볼 수 있습니다.

이제 학습된 SentencePiece 모델을 활용해보겠습니다.

 

s = spm.SentencePieceProcessor()
s.Load('korean_spm.model')

# SentencePiece 를 활용한 sentence -> encoding
tokenIDs = s.EncodeAsIds('아버지가 방...에 들어가신다.')
print(tokenIDS)

# SentencePiece 를 활용한 sentence -> encoded pieces
print(s.SampleEncodeAsPieces('아버지가방에들어가신다.', 2, 0.0))

# SentencePiece 를 활용한 encoding -> sentence 복원
print(s,DecodeIds(tokenIDs)

샘플로 뽑아본 결과로 볼 때 SentencePiece의 성능이 나쁘지 않은 것을 볼 수 있습니다.

그럼 학습시킨 SentencePiece를 활용해 앞에서 만들었던 tokenize() 함수와 비슷한 기능을 가진 sp_tokenize() 함수를 만들어 보겠습니다. sp_tokenize() 함수는 아래 기능을 갖고 있습니다.

  1. 매개변수로 토큰화 된 문장을 리스트로 전달하는 게 아니라 온전한 문장의 리스트를 전달합니다.
  2. 생성된 vocab파일을 읽어와 {단어 : 인덱스} 형태를 가지는 word_index 사전과 {인덱스 : 단어} 형태를 가지는 index_word 사전을 생성하고 반환합니다.
  3. tensor는 토큰화한 후 인코딩 된 문장입니다.(정수 인덱스화)
def sp_tokenize(s, corpus):
  tensor = []
  
  for sen in corpus:
    tensor.append(s.EncodeAsIds(sen))
    
  with open('./korean_spm.vocab', 'r') as f:
    vocab = f.readlines()
    
  word_index = {}
  index_word = {}
  
  for idx, line in enumerate(vocab):
    word = line.split('t')[0]
    
    word_index.update({word:idx})
    index_word.update({idx:word})
    
  tensor = tensorflow.keras.preprocessing.sequence.pad_sequences(tensor, padding='post')
  
  return tensor, word_index, index_word

실습

앞에선 형태소 분석기와 subword 기반의 SentencePiece를 이용하는 방법에 대해 알아봤습니다.

이제 앞에서 배운 방법으로 네이버 영화 리뷰 감정 분석을 해보겠습니다.

그리고 KoNLPy의 Mecab의 결과와 비교해보겠습니다.

SentencePiece(unigram)

먼저 데이터를 불러옵니다. 사용 데이터는 Toy Project에서 진행했던 네이버 리뷰 데이터를 사용하겠습니다.

import pandas as pd
import numpy as np
import tensorflow as tf

train_data = pd.read_table('.../ratings_train.txt')
test_data = pd.read_table('.../ratings_test.txt')

all_data = pd.concat([train_data, test_data], ignore_index=True, axis=0)

# 결측치 제거
all_data.dropna(axis=0, inplace=True)

# 중복치 제거
all_data.drop_duplicates('document', keep='first')

# all_data의 document 컬럼을 sentence에 할당
sentence = all_data['document']

sentence

리뷰 데이터의 문장 길이 분포를 확인해보겠습니다.

위에서 사용한 방법을 이용하겠습니다.

import matplotlib.pyplot as plt

min_len = 999
max_len = 0
sum_len = 0

for s in sentence:
  if len(s) < min_len :
    min_len = len(s)
  if len(s) > max_len :
    max_len = len(s)
  sum_len += len(s)

print('Min length : ', min_len)
print('Max length : ', max_len)
print('Average length : ', sum_len // len(sentence))

sen_length_cnt = [0] * max_len
for sen in sentence:
  sen_length_cnt[len(sen)-1] += 1

plt.bar(range(max_len), sen_length_cnt, width=1.0)
plt.show()

문장을 전부 사용하지 않고 길이가 10 ~ 140인 문장들만 사용하겠습니다.

max_len = 140
min_len = 10

filtered_sen = []
filtered_target = []

target = np.array(all_data['label'])

for s, t in zip(sentence, target):
  if (len(str(s)) < max_len) and (len(str(s)) >= min_len) :
    filtered_sen.append(s)
    filtered_target.append(t)
    
print(len(filtered_sen))
print(len(filtered_target))

길이를 기준으로 나눈 데이터는 총 180320개의 데이터가 있습니다.

이제 이 데이터를 가지고 SentencePiece 모델을 학습해주겠습니다.

학습 방법은 앞에서 봤지만 여기서는 파라미터가 뭘 의미하는지 알아보겠습니다.

spm.SentencePieceTrainer.Train(
           '--input={} 
            --model_prefix=korean_spm 
            --vocab_size={} 
            --model_type=unigram'.format(temp_file_path, vocab_size))
  • --input : 한 줄에 한 문장이 있는 raw corpus 파일 경로를 넣어줍니다. 토큰화, 전처리, 정규화를 할 필요는 없습니다. 
  • --model_prefix : 훈련 후에 만들어지는 파일의 이름입니다. ***. model, ***. vocab 파일이 생성됩니다.
  • --vocab_size : 단어장의 크기입니다. 예를 들어 8000, 16000, 32000으로 설정할 수 있습니다.
  • --model_type : 모델의 타입을 설정할 수 있습니다. 기본 값으로 unigram이 설정되어 있고, bpe, char 또는 word로 변경 가능합니다. 단, word 타입을 사용하려면 입력 문장이 토큰화가 되어 있어야 합니다.

이제 모델을 학습시키겠습니다.

import sentencepiece as spm
temp_file_path = '.../ratings_test.txt.temp'

vocab_size = 16000

# 지정한 경로에 한 줄에 문장 하나 있는 파일을 만들어줍니다.
with open(temp_file_path, 'w') as f:
  for row in filtered_sen:
    f.write(str(row) + '\n')

spm.SentencePieceTrainer.Train(
    '--input={} --model_prefix=korean_spm --vocab_size={} --model_type=unigram'.format(temp_file_path, vocab_size)
)

!ls -l korean_spm*

학습된 모델과 앞서 만들어 둔 함수를 이용해 토큰화를 진행하겠습니다.

def sp_tokenize(s, corpus):
  tensor = []

  for sen in corpus:
    tensor.append(s.EncodeAsIds(sen))
  
  with open("./korean_spm.vocab", 'r') as f:
    vocab = f.readlines()
  
  word_index = {}
  index_word = {}

  for idx, line in enumerate(vocab):
    word = line.split("\t")[0]

    word_index.update({word:idx})
    index_word.update({idx:word})
  
  tensor = tensorflow.keras.preprocessing.sequence.pad_sequences(tensor, padding='pre')

  return tensor, word_index, index_word
  
# 전체 데이터 전처리 + 토큰화
tensor, word_index, index_word = sp_tokenize(s, filtered_sen)

print(tensor.shape)
print(tensor)
print(len(word_index))
print(len(index_word))

데이터는 준비됐으니 이제 감정 분류 모델을 설계하고 학습시키겠습니다.

 

훈련, 평가 데이터의 비율은 8:2로 하겠습니다.

# 데이터 나누기
from sklearn.model_selection import train_test_split

x_train, val_x, y_rain, val_y = train_test_split(tensor, filtered_target, test_size=0.2)
y_train = np.array(y_train)
val_y = np.array(val_y)
# LSTM 모델 설계
import tensorflow as tf

vocab_size = vocab_size
word_vector_dim = 200

model_lstm = tf.keras.Sequential()
model_lstm.add(tf.keras.layers.Embedding(vocab_size, word_vector_dim, input_shape=(None,)))
model_lstm.add(tf.keras.layers.LSTM(128))
model_lstm.add(tf.keras.layers.Dense(32, activation='relu'))
model_lstm.add(tf.keras.layers.Dense(1, activation='sigmoid')) # 최종 출력은 긍정/부정을 나타내는 1dim 입니다.
model_lstm.summary()

# 모델 훈련

epochs = 2

model_lstm.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])

history_lstm = model_lstm.fit(x_train,
                              y_train,
                              epochs=epochs,
                              batch_size=128,
                              validation_data=(val_x, val_y),
                              verbose=1)

여러 번의 시도가 있었지만 그중 제일 잘 나온 결과로만 작성했습니다. 

중간 시도에 대한 결과는 표로 정리했습니다.

Unigram 시도 1 시도 2 시도 3 시도 4 시도 5 시도 6 시도 7 시도 8
문장 길이 10~60 5~100 5~100 all all 5~80 10~80 10~140
단어장 크기 16000 16000 16000 16000 32000 32000 32000 16000
패딩 위치 post post pre pre pre pre pre pre
임베딩 차원 200 200 200 200 200 200 100 200
배치 사이즈 128 128 128 128 128 128 128 128
val_loss .9434 .6932 1.3404 - 1.1246 1.2195 .7159 .3107
val_acc .8228 .4957 .8271 - .8195 .8295 .8366 .8657

Mecab

Mecab을 사용할 때와 SentencePiece를 사용할 때의 차이는 데이터 전처리와 토큰화에서만 있습니다.

같은 방식으로 진행되는 부분은 스킵하고 다른 부분만 작성하겠습니다.

 

Mecab에서는 전처리를 따로 진행해줘야 하기 때문에 함수를 구현하여 전처리를 진행해줍니다.

구현한 함수는 다음과 같은 기능을 포함합니다.

  1. 데이터의 중복 제거
  2. 결측치 제거
  3. 한국어 토크나이저로 토큰화
  4. 불용어 제거
  5. 사전 구성
  6. 텍스트 스트링을 사전 인덱스 스트링으로 변환
  7. x_train, y_train, val_x, val_y, word_to_index 리턴

여기서 불용어 제거에 사용할 불용어 리스트는 https://gist.github.com/chulgil/d10b18575a73778da4bc83853385465c#file-stopwords-txt 이곳에서 가져왔습니다.

# 가져온 불용어 파일로 불용어 리스트 생성
stop_file_path = '.../stopwords.txt'

with open(stop_file_path) as f:
  lines = f.read().splitlines()

stopwords_list = lines
# load data 함수

from konlpy.tag import Mecab
import numpy as np
from collections import Counter

tokenizer_mecab = Mecab()

def load_data(train_data, test_data, num_words=16000):
  len_train = len(train_data)

  all_data = pd.concat([train_data, test_data], axis=0, ignore_index=True)
  all_data = all_data.drop_duplicates('document', keep='first')
  all_data = all_data.dropna(axis=0)

  sentence_list = list(all_data['document'])

  token_list = []
  for sen in sentence_list:
    tokenize_sentence = tokenizer_mecab.morphs(sen)
    tokenize_sentence = [word for word in tokenize_sentence if word not in stopwords_list]
    token_list.append(tokenize_sentence)
  
  x_train = token_list[:len_train]
  val_x = token_list[len_train:]

  words = np.concatenate(x_train).tolist()
  counter = Counter(words)
  counter = counter.most_common(15000-4)
  vocab = ['<PAD>', '<BOS>', '<UNK>', '<UNUSED>'] + [key for key, _ in counter]
  
  word2index = {word : index for index, word in enumerate(vocab)}

  def wordlist_to_indexlist(wordlist):
    return [word2index[word] if word in word2index else word2index['<UNK>'] for word in wordlist]
  
  x_train=np.array(list(map(wordlist_to_indexlist, x_train)), dtype=object)
  val_x = np.array(list(map(wordlist_to_indexlist, val_x)), dtype=object)

  return x_train, np.array(list(all_data['label'][:len_train]), dtype=object), val_x, np.array(list(all_data['label'][len_train:]), dtype=object), word2index
x_train, y_train, val_x, val_y, word2index = load_data(train_data, test_data)

index2word = {index : word for word, index in word2index.items()}

토큰화 된 문장들의 길이 분포를 확인하고 패딩을 위한 최대 문장 길이를 지정해주겠습니다.

total_data_text = list(x_train) + list(val_x)

# 텍스트데이터 문장길이의 리스트를 생성
num_tokens = [len(tokens) for tokens in total_data_text]
num_tokens = np.array(num_tokens)

# 문장길이의 평균값, 최대값, 표준편차를 계산
print('문장길이 평균 : ', np.mean(num_tokens))
print('문장길이 최대 : ', np.max(num_tokens))
print('문장길이 표준편차 : ', np.std(num_tokens))

# 최대 길이를 (평균 + 2 * 표준편차) 로 한다면 남는 문장의 비율은?
max_tokens = np.mean(num_tokens) + 2 * np.std(num_tokens)
maxlen = int(max_tokens)
print('pad_sequences maxlen : ', maxlen)
print('전체 문장의 {}%가 maxlen 설정값 이내에 포함됩니다. '.format(np.sum(num_tokens < max_tokens) / len(num_tokens)))

# 패딩 진행

import tensorflow as tf

# 문장의 마지막 입력이 최종 state에 영향을 가장 크게 미치기 때문에 패딩 적용은 pre(앞쪽)으로 해줍니다.
x_train = tf.keras.preprocessing.sequence.pad_sequences(x_train,
                                                        value=word2index['<PAD>'],
                                                        padding = 'pre',
                                                        maxlen = maxlen)
 

val_x = tf.keras.preprocessing.sequence.pad_sequences(val_x,
                                                       value=word2index["<PAD>"],
                                                       padding='pre',
                                                       maxlen=maxlen)
''' 
   모델을 돌리는데 계속 int type의 자료는 tensor로 변환할 수 없다고 뜨길래
   자료형을 np.float32 로 바꿔줬습니다.
   왜 전에 할 때는 잘 되다가 여기선 또 오류가 나는지,,,
   여기서 머리가 좀 아팠네요... ㅎㅎㅎ
'''


x_train = x_train.astype(np.float32)
y_train = y_train.astype(np.float32)
val_x = val_x.astype(np.float32)
val_y = val_y.astype(np.float32)
vocab_size = 16000
word_vector_dim = 200

model_lstm_mecab = tf.keras.Sequential()
model_lstm_mecab.add(tf.keras.layers.Embedding(vocab_size, word_vector_dim, input_shape=(None,)))
model_lstm_mecab.add(tf.keras.layers.LSTM(128))
model_lstm_mecab.add(tf.keras.layers.Dense(32, activation='relu'))
model_lstm_mecab.add(tf.keras.layers.Dense(1, activation='sigmoid')) # 최종 출력은 긍정/부정을 나타내는 1dim 입니다.
model_lstm_mecab.summary()

model_lstm_mecab.compile(optimizer = 'adam',
                         loss = 'binary_crossentropy',
                         metrics=['accuracy'])

epochs = 2

history_lstm_mecab = model_lstm_mecab.fit(x_train,
                                          y_train,
                                          epochs=epochs,
                                          batch_size=64,
                                          validation_data=(val_x, val_y),
                                          )

전체 결과 비교

  SentencePiece(unigram) SentencePiece(BPE) Mecab
Vocab size 16000 16000 16000
문장 길이 10~140 10~140 (토큰 개수 기준) 39
패딩 위치 pre pre pre
임베딩 차원 200 200 200
배치 사이즈 128 128 64
Validation loss .3107 .3282 .3204
Validation accuracy .8657 .8649 .8635

 

회고

 

이번에 처음 SentencePiece 모델을 사용해봤는데 전처리 없이 그냥 raw text를 그대로 받아서 학습하고 토큰화와 인덱싱까지 해주는 게 너무 편했습니다. Mecab을 사용하려면 기본적인 불용어 제거, 구두점 제거 등의 전처리를 진행해줘야 하는 귀찮음(?)이 있었는데 그게 없는 게 가장 컸습니다. 거기에 성능까지 준수하기 때문에 정말 안 쓸 이유가 없는 모델인 것 같습니다.

그리고 처음에 LMS에서는 패딩을 'post'로 하는데 LSTM에 들어갈 때 패딩은 'pre'로 하는 게 더 좋다고 들어서 왜 이렇게 하는 거지?라는 생각이 들었습니다.
그래도 일단 'post'로 진행해봤는데 성능이 너무 안 좋아서 'pre'로 바꿔주니 확실히 성능이 좋아졌습니다.
그리고 문장의 길이에 따른 성능을 볼 때도 처음에 문장을 전부 다 쓰면 데이터가 많으니까 좋겠지?? 하고 전부 다 사용했습니다. 그랬더니 그래프가 미친듯한 과적합 양상을 보여서 놀래서 중단했습니다. ㅎㅎ;
그 후로 5 ~ 80, 10 ~ 80, 10~140으로 길이를 바꿔가며 진행했는데 문장의 길이가 길어지는 것은 성능에 그렇게 큰 영향을 끼치진 않았지만 길이가 너무 짧은(10 미만) 문장들은 오히려 성능에 악영향을 끼치는 모습을 보였습니다. 앞으로 문장의 길이가 너무 짧은 것들은 얄짤없이 다 버려야겠습니다.

이번에 처음으로 Mecab대신 다른 SentencePiece라는 모델을 사용해봤는데 전처리가 필요 없이 아주 준수한 성능을 내는 모델이란 걸 알았고 다음에 데이터 전처리를 할 때는 웬만해서는 SentencePiece 모델을 사용할 것 같습니다.

'AI > NLP Study' 카테고리의 다른 글

[NLP] 임베딩 편향성  (0) 2022.10.02
[NLP] 워드 임베딩  (1) 2022.10.01
[NLP] 텍스트 카테고리 분류  (1) 2022.09.29
[NLP] 텍스트 벡터화  (0) 2022.09.27
[NLP] 자연어 전처리 : 토큰화  (3) 2022.09.21