Data-Science/NLP

문장 관계 분류 모델, KoBERT로 돌려보다

제일리 2022. 1. 31. 10:42

0. 목차

  • Bert를 사용하는 이유
  • Bert 활용 분야
  • 코드 (tensorflow)

1. BERT를 사용하는 이유

"Attention is all you need"이라는 논문이 나온 이후로 자연어처리(NLP)분야는 더욱 빠르게 발전하기 시작했다.

논문에서 Transformer라고 이름을 붙힌 이 모델은 Attention layer를 활용하여 인코더(Encoder)와 디코더(Decoder)를 만들어 번역모델로도 사용하곤 했다.

추후 이 모델의 인코더 혹은 디코더만을 활용하여, 인코더(Encoder)로 BERT와 같은 모델을 만들었고, 디코더(Decoder)로 GPT와 같은 모델을 만들었다. 

각각의 장점으로 인코더(Encoder)는 문장의 문맥을 인식하는 데 유리하고, 디코더(Decoder)는 문장 생성에 유리하다. 우리가 흔히 들어본 BERT모델은 문장 속의 문맥을 잘 이해하고, 글 속의 중요 문장을 색출해내는 역할을 잘하며, GPT 모델은 text generation을 잘한다. 

 

이제 우리가 실생활에 BERT를 사용해야 하는 이유를 설명해보도록 하겠다.

 

# Bert란?

BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding

BERT는 사전학습된 양방향 transformer기반의 언어 모델이다.

 

# 다른 머신러닝보다 자연어 처리가 더 힘들다

일반 머신러닝 과제들의 과정을 살펴보면, 전처리, 정규화, 모델학습이 있다. 하지만 자연어처리는 이 과정이 조금 더 복잡하고 시간이 오래 걸린다. 자연어 처리에 있어 전처리 과정의 기본적인 과정을 말해보자면 문장 길이 조정, 불용어 제거, 형태소 분석 등을 하고, 글자의 수치화 작업인 임베딩 작업도 진행을 한다. 그 후 RNN기반인 SimpleRNN, LSTM, GRU등의 모델에 입력을 했지만, 이 모든 과정이 많은 시간을 소요했고, 정확도 향상을 위해 추가적인 전처리가 필요했다. 아쉽게도 한글의 경우 영어이 비해 자연어처리가 어려운 점이 존재해 정확도 향상에 많은 한계가 있다.

 

# 사전 학습된 (pretrained)모델로 충분히 좋은 결과를 낼 수 있다

이렇게 시간이 오래 걸리는 전처리와 크게 높아지지 않는 정확도를 보고 포기하게 되는 경우가 많다. 하지만 이미 사전에 훈련된 모델을 이용하여 미세조정(fine-tune)을 하게 된다면 큰 어려움 없이 원하는 결과값을 낼 수 있게 된다. 또한, 모델 안에서 임베딩 작업을 실행해주기 때문에 모델 구조에 대해서 이해한다면 시간을 더 절약할 수 있게 된다.

https://arxiv.org/pdf/1810.04805
https://arxiv.org/pdf/1810.04805

사전학습된 모델 (pretrained model)과 미세조정 (fine-tune)
사전학습된 모델이란 쉽게 말해서 방대한 데이터에 대해서 이미 학습을 마친 모델을 말한다. 우리는 이런 모델을 가지고 우리 데이터에
대해서 최적화 할 수 있도록 추가 학습을 하게 되는데, 이 과정에서 모델의 가중치가 우리 데이터에 대해 업데이트 되는 과정을  "미세조정"이라고 한다.

2. BERT 활용 분야

https://arxiv.org/pdf/1810.04805

BERT로는 총 4가지 task를 수행할 수 있다.

-  Sentence Pair Classification ( 두 문장의 문맥 관계 분류 문제 )

- Single Sentence Pair Classification ( 한 문장의 감정 분류 문제 )

- SQuad [Question & Answering]  ( 질문에 대한 답 유추 )

- Single Sentence Tagging ( 각 토큰에 대한 객체명 출력 )

 

밑에 있는 코드는 첫 번째 task인 두 문장의 관계 분류 모델을 작성한 것이다.

 

3. 코드 (tensorflow)

 

자연어 처리에 있어 한국어와 영어는 조금 다르다. 이에 대해 가장 보편적인 이유로는 한국어는 띄어쓰기가 없어도 말이 되는 경우가 많다, 그리고 영어는 알파벳들의 조합으로 단어가 형성되는 반면, 한국어는 한 글자 단어가 존재하고, 한 글자로도 뜻을 가지고 있다.

 

감사하게도 SKTBrain에서 KoBERT를 베포하고 있고, 이를 활용하여 문장 관계 분류모델 코드를 작성하고자 한다.

현 KoBERT는 pytorch기반으로 베포되고 있다,

하지만 필자는 아직 tensorflow가 편해 KoBERT를 tensorflow로 사용한 코드중 주요 파트를 작성하고자 한다

(코드를 쉽게 이해하실 수 있으신 분들은 본 글의 코드를 참고하셔도 좋으며, 설명이 필요하신 분들은  맨 아래 블로그 글을 참고하길 바랍니다.)

 

# 깃 클론 및 필요 라이브러리 설치

!git clone https://github.com/SKTBrain/KoBERT.git
!pip install 'git+https://github.com/SKTBrain/KoBERT.git#egg=kobert_tokenizer&subdirectory=kobert_hf'

!pip install sentencepiece
!pip install transformers
# 주의: 꼭 sentencepiece를 transformer전에 import해주셔야 합니다.
import sentencepiece
import tensorflow as tf
import transformers 
from transformers import TFBertModel

from kobert_tokenizer import KoBERTTokenizer
#SKTBrain/KoBERT의 python파일 사용으로 현재 경로 유의해주시길 바랍니다.

 

 

- BERT 모델에 인풋을 위한 데이터 파이프라인 생성 (BertSemanticDataGenerator)

class BertSemanticDataGenerator(tf.keras.utils.Sequence):
  '''Generate Batches of data

  Args: 
    sentence_pairs: Array of premise and hypothesis input sentences.
    labels: Array of labels.
    batch_size: Integer batch size.
    shuffle: boolean, whether to suffle the data.
    include_targets: boolean, whether to include the labels.

  Returns:
    Tuples '([input_ids, attention_mask, 'token_type_ids], labels')
      or just '[input_ids, attention_mask, 'token_type_ids]' if 'include_targets=False')
  '''
  def __init__(self, sentence_pairs, labels, batch_size=batch_size, shuffle=True, include_targets=True):
    self.sentence_pairs = sentence_pairs
    self.labels = labels
    self.shuffle = shuffle
    self.batch_size = batch_size
    self.include_targets = include_targets
    #Load out BERT Tokenizer to encode the text.
    #We will use SKT-brain KOBERT pretrined model.

    self.tokenizer = KoBERTTokenizer.from_pretrained('skt/kobert-base-v1')
    self.indexes = np.arange(len(self.sentence_pairs))
    self.on_epoch_end()

  def __len__(self):
    # Denote the number of batches per epoch.
    return len(self.sentence_pairs) // self.batch_size
  
  def __getitem__(self, idx):
    #Retrieve the batch of index.
    indexes = self.indexes[idx * self.batch_size : (idx + 1) * self.batch_size]
    sentence_pairs = self.sentence_pairs[indexes]

    #With BERT tokneizer's batch_encode_plus batch of bothe the sentence are
    # encoded together and separated by [sep] token.
    encoded = self.tokenizer.batch_encode_plus(
        sentence_pairs.tolist(),
        add_special_tokens=True,
        max_length=max_length,
        return_attention_mask=True,
        return_token_type_ids=True,
        pad_to_max_length=True,
        return_tensors='tf'
    )

    #Convert batch of encoded featurs to numpy array
    input_ids = np.array(encoded['input_ids'], dtype='int32')
    attention_masks = np.array(encoded['attention_mask'], dtype='int32')
    token_type_ids = np.array(encoded['token_type_ids'], dtype='int32')

    #Set to tru if data generator is used for training/validation.
    if self.include_targets:
      labels = np.array(self.labels[indexes], dtype='int32')
      return [input_ids, attention_masks, token_type_ids], labels
    else:
      return [input_ids, attention_masks, token_type_ids]

  def on_epoch_end(self):
    #Shuffle indexes after each epoch if suffle is set to True.
    if self.shuffle:
      np.random.RandomState(42).shuffle(self.indexes)
#GPU사용 여부 확인
device_name = tf.test.gpu_device_name()
if device_name != '/device:GPU:0':
  raise SystemError('GPU device not found')
print('Found GPU at: {}'.format(device_name))
with tf.device('/device:GPU:0'): #코랩 GPU사용하여 학습
  #Encoded token ids from BERT tokenizer.
  input_ids = tf.keras.layers.Input(shape=(max_length,), dtype=tf.int32, name='input_ids')

  #Attention masks indicates to the model which tokens should be attended to.
  attention_masks = tf.keras.layers.Input(shape=(max_length,), dtype=tf.int32, name='attention_masks')

  #Token type ids are binary masks identifying different sequences in the model.
  token_type_ids = tf.keras.layers.Input(shape=(max_length,), dtype=tf.int32, name='token_type_ids')

  #Loading pretrained BERT model.
  bert_model = TFBertModel.from_pretrained('skt/kobert-base-v1',from_pt=True)
  #Freeze the BERT model to reuse the pretrained features without modifying them.
  bert_model.trainable = False

  bert_output = bert_model(input_ids, attention_mask=attention_masks, token_type_ids=token_type_ids)

  sequence_output = bert_output.last_hidden_state
  pooled_output = bert_output.pooler_output
  #Add trainable layers on top of frozen layers to adapt the pretrained features on the new data
  bi_lstm = tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64, return_sequences=True))(sequence_output) #Changed to GRU 2022/1/31
  #Applying hybrid pooling apporach to bi_lstm sequence output.
  avg_pool = tf.keras.layers.GlobalAveragePooling1D()(bi_lstm)
  max_pool = tf.keras.layers.GlobalMaxPooling1D()(bi_lstm)
  concat = tf.keras.layers.Concatenate()([avg_pool,max_pool])
  dropout = tf.keras.layers.Dropout(0.3)(concat)
  output = tf.keras.layers.Dense(3, activation='softmax')(dropout)
  model = tf.keras.models.Model(inputs=[input_ids, attention_masks, token_type_ids], outputs=output)

  model.compile(
      optimizer=tf.keras.optimizers.Adam(),
      loss='categorical_crossentropy',
      metrics=['acc']
  )

# print(f'Strategy: {strategy}')
model.summary()

 

 

- train/validation data 준비

'''
- contradiction : 0
- entailment : 1
- neutral : 2

label : one-hot encoding된 레이블 값
'''
train_data = BertSemanticDataGenerator(
    sentence_pairs=train[['premise','hypothesis']].values.astype('str'),
    labels=train_label,
    batch_size=batch_size,
    shuffle=True
)

valid_data = BertSemanticDataGenerator(
    sentence_pairs=validation[['premise','hypothesis']].values.astype('str'),
    labels=valid_label,
    batch_size=batch_size,
    shuffle=False
)

 

- 모델 학습

history = model.fit(train_data, 
                    epochs=4,
                    validation_data=valid_data,
                    use_multiprocessing=True,
                    verbose=1,
                    workers=-1,
                    )

 

 

* 문장 관계 분류 모델 전체 코드에 대한 자세한 리뷰

2022.01.31 - [NLP] - SKTBrain KoBERT 텐서플로우로 돌리기

'Data-Science > NLP' 카테고리의 다른 글

LSTM으로 스팸메일 분류하기  (0) 2022.01.31
SKTBrain KoBERT 텐서플로우로 돌리기  (0) 2022.01.31