Seq2Seq 어텐션 형태소 분석

본 블로그에 Seq2Seq 모델을 활용해서 간단한 문장을 생성한다던가 번역을 해보는 예제를 수행했습니다. 또 Seq2Seq에 Attention을 적용해서 문장생성을 테스트해 보기도 했습니다.

이번에는 Seq2Seq 어텐션을 활용해서 형태소 분석을 수행하는 예제를 문들어보겠습니다. 수행하는 방법은 이전에 수행했던 예제들과 아주 유사해서 이전 예제에서 활용했던 Word Embedding, Encoder, Decoder, RNN 모델을 그대로 사용하겠습니다.

해당 방법은 여러 연구자들에 의해서 연구되고 있습니다.
ETRI(한국전자통신연구원)에서도 해당 모델을 활용한 연구(Seq2Seq 주의집중 모델을 이용한 형태소 분석 및 품사 태깅, 2016년)를 수행했습니다.
이 외에도 포항공대에서도 “Sequence-to-sequence 기반 한국어 형태소 분석 및 품사 태깅”이라는 연구가 있었습니다.

먼저 형태소에 대한 정의는 아래와 같습니다.

형태소(形態素, 영어: morpheme)는 언어학에서 (일반적인 정의를 따르면) 일정한 의미가 있는 가장 작은 말의 단위로 발화체 내에서 따로 떼어낼 수 있는 것을 말한다. 즉, 더 분석하면 뜻이 없어지는 말의 단위이다. 음소와 마찬가지로 형태소는 추상적인 실체이며 발화에서 다양한 형태로 실현될 수 있다. [위키백과 : 형태소]

간단히 말하면 분석의 대상이 되는 문장이 입력 됐을 경우에 “일정한 의미가 있는 가장 작은 말의 단위”로 분할 하는 것이라고 할 수 있습니다.

해당 예제는 다음과 같은 방법으로 수행합니다. 먼저 보통의 짧은 문장 50개를 생성합니다. 생성한 문장을 KoNLPy 중 Okt() 태깅 클래스를 활용하여 형태소 분석을 수행합니다. 예를 들어서 [‘요즘도 많이 바쁘세요?’,’구두를 신고 싶어요.’,’운동화를 신고 싶어요.’,’엄마가 좋아요?’,’아빠가 좋아요?’]와 같은 문장 리스트가 주어졌다고 할 때에 이를 형태소 분석을 하게 되면 아래와 같은 형태로 데이터가 출력된다.

[요즘/Noun, 도/Josa, 많이/Adverb, 바쁘세요/Adjective, ?/Punctuation]
[구두/Noun, 를/Josa, 신고/Noun, 싶어요/Verb, ./Punctuation]
[운동화/Noun, 를/Josa, 신고/Noun, 싶어요/Verb, ./Punctuation]
[엄마/Noun, 가/Josa, 좋아요/Adjective, ?/Punctuation]
[아빠/Noun, 가/Josa, 좋아요/Adjective, ?/Punctuation]
https://konlpy-ko.readthedocs.io/ko/v0.4.3/

KoNLPy에 대해서 더 자세히 알아보고자 하시는 분은 위의 홈페이지에서 자료를 검색해보시기 바랍니다.

이제 입력된 원문을 Source에 입력하고 형태소 분석한 결과를 Target에 입력하는 것으로 학습 데이터를 생성하겠습니다. 이렇게 되면 Source 데이터를 Encoder에 입력하고 분석 결과를 Decoder에 입력해서 학습합니다.

먼저는 인코더에 넣을 텍스트 데이터를 숫자형태로 바꿔 주기 위한 클래스를 선언합니다. 해당 클래스는 문장의 시작<SOS, Start of Sentence>과 끝<EOS, End of Sentence>을 나태는 변수를 선언하는 것으로 시작합니다. 먼저 문장이 입력되면 음절 단위로 분리하고 음절이 존재 할 경우는 해당 어절의 카운트를 1 증가 시키고 없을 경우 dict에 음절을 추가합니다.

source_vocab은 인코딩 문장 즉, 원어절이 들어갑니다. 반면 target_vocab은 형태소 정보가 들어간 어절이 입력됩니다.

SOS_token = 0
EOS_token = 1

class Vocab:
  def __init__(self):
    self.vocab2index = {'<SOS>':SOS_token, '<EOS>':EOS_token}
    self.index2vocab = {SOS_token:'<SOS>', EOS_token:'<EOS>'}
    self.vocab_count = {}
    self.n_vocab = len(self.vocab2index)
    
  def add_vocab(self, sentence):
    for word in sentence.split(' '):
      if word not in self.vocab2index:
        self.vocab2index[word] = self.n_vocab
        self.vocab_count[word] = 1
        self.index2vocab[self.n_vocab] = word
        self.n_vocab += 1
      else:
        self.vocab_count[word] += 1

source_vocab = Vocab()
target_vocab = Vocab()

전체적인 흐름은 이전에 테스트했던 내용과 비슷하기 때문에 자세한 설명은 생략하고 변경된 내용만 정리합니다. 인코더는 131×5의 lookup 테이블에 맵핑됩니다. 즉, GRU에 131개의 input_size를 보내지 않고 5개의 값만을 사용한다는 의미입니다. GRU 셀(Cell)을 보면 설명드린대로 입력과 출력이 동일하게 정의했고 4개의 multi-layer로 구성했습니다. batch_first를 True로 설정했습니다.

Encoder(
  (embedding): Embedding(131, 5)
  (gru): GRU(5, 5, num_layers=4, batch_first=True)
)

디코더는 Attention 모델을 사용하여 모델을 설계합니다. 입력값 135를 받아서 5개의 입력으로 내보냅니다. 135는 target_vocab의 크기입니다. 5로 입력하는 것은 decoder가 이전 단계 encoder의 hidden_state를 입력으로 받기 때문에 encode와 동일한 사이즈로 정의해줍니다. attn Linear에서는 decoder에 입력되는 값과 이전 단계의 hidden 값을 합하여서 target의 max_length 값인 7로 정의합니다.

이것은 attention 모델에서 중요한 과정이라고 할 수 있는 attention weight(어떤 값에 집중할 것인가?)에 대한 부분을 정의하는 부분입니다. 이제 이 attention weight 값과 encoder의 output 데이터들을 곱하여 하나의 matrix를 생성합니다. 이 값을 decoder에 입력되는 값과 함께 GRU 셀에 입력 데이터로 사용합니다.

이렇게 나온 출력 값을 Linear 모델을 거쳐 target_vocab 사이즈와 동일하게 맞춰주고 출력값의 index 값을 찾아 일치되는 값을 출력합니다.

AttentionDecoder(
  (embedding): Embedding(135, 5)
  (attn): Linear(in_features=10, out_features=7, bias=True)
  (attn_combine): Linear(in_features=10, out_features=5, bias=True)
  (dropout): Dropout(p=0.1, inplace=False)
  (gru): GRU(5, 5, num_layers=4, batch_first=True)
  (out): Linear(in_features=5, out_features=135, bias=True)
)

글로 표현하는 것이 길뿐 코드로 표현하면 이전의 attention 모델과 동일합니다. attention 모델은 기본 seq2seq 모델에서 사용했던 context vector를 사용하지 않고 encoder의 각 output 결과를 사용하는 것이 차이가 있습니다.

class Test():

  def __init__(self, sentences, source_vocab, target_vocab, encoder, decoder):
    self.sentences = sentences
    self.vocab = source_vocab
    self.target_vocab = target_vocab
    self.encoder = encoder
    self.decoder = decoder

  def tensorize(self, sentence):
    idx = [self.vocab.vocab2index[word] for word in sentence.split(' ')]
    return idx

  def run(self):
    x_train = [self.tensorize(sentence) for sentence in self.sentences]
    text = []
    
    for x in x_train:
      decoded_word=[]      
      _x = torch.tensor(x, dtype=torch.long).view(-1,1)      
      encoder_hidden = self.encoder.initHidden()
      encoder_outputs = torch.zeros(config.max_length, self.encoder.hidden_size)
      
      for ei in range(_x.size(0)):
          encoder_output, encoder_hidden = self.encoder(_x[ei], encoder_hidden)
          encoder_outputs[ei] = encoder_output[0,0]

      decoder_input = torch.tensor([SOS_token], dtype=torch.long)
      decoder_hidden = encoder_hidden

      for di in range(config.max_length):
        decoder_output, decoder_hidden, decoder_attention = self.decoder(decoder_input, decoder_hidden, encoder_outputs)
        i_val,i_ndx = decoder_output.data.topk(1)
        t_word = self.target_vocab.index2vocab[i_ndx.item()]
        decoded_word.append(t_word)

        if _x.size(0) > di: 
          if _x[di] < self.target_vocab.n_vocab:
            decoder_input = _x[di] 
        else: 
          decoder_input = i_ndx.squeeze().detach()

        # 문장 마침표 break
        if t_word == './Punctuation': break
        # <EOS> break
        if i_ndx == 1: break
        
      text.append(decoded_word)
    return text

  def predict(self):
    return self.run()

이제 구축된 모델을 Test Class를 활용해서 테스트해보겠습니다.

sentences = ['많이 추워요.','김밥이 좋아요.','앞으로 오세요.','시험 공부를 해요.','책을 읽어보자.','처음 뵙겠습니다.']
test = Test(sentences, source_vocab, target_vocab, encoder, decoder)
predict = test.predict()
for ndx,(i,j) in enumerate(zip(sentences, predict)):
  print(ndx, i,j)

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다