Data-Science/NLP

SKTBrain KoBERT 텐서플로우로 돌리기

제일리 2022. 1. 31. 13:49

1. 데이터  소개

COLUMN: Premise | Hypothesis | label 

LABEL : "Contradiction", "Entailment", "Neutral"

train.csv : 28998 row, 3 col

test.csv: 1666 row, 3col

submission.csv : 1666, 1col (label)

train = pd.read_csv('train_data.csv')
test = pd.read_csv('test_data.csv')
submit = pd.read_csv('submission.csv')

2. Validation 데이터 생성

** train 데이터와 validatoin데이터를 생성할 때 레이블값의 불균형이 생기면 안되기 때문에 랜덤 샘플링으로 데이터셋 구분

import random
random.seed(124)

index = train['index'].to_list()
len_index = len(index)

#train의 20%만 validation으로 지정
num_of_val = np.int32(len_index * 0.2) 
val_index = random.sample(index, num_of_val)

validation = train.iloc[val_index]
train = train.drop(train.index[val_index])

3. 레이블 값 숫자변경 및 원핫 인코딩 (one-hot encoding)

from sklearn.preprocessing import LabelEncoder
from tensorflow.keras.utils import to_categorical

#레이블 숫자화
encoder = LabelEncoder()
encoder.fit(train['label'])
train_label = encoder.transform(train['label'])
valid_label = encoder.transform(validation['label'])

#레이블 원핫 인코딩
train_label = to_categorical(np.asarray(train_label))
valid_label = to_categorical(np.asarray(valid_label))

4.  SKTBrain의 KoBERT 깃 클론 및 필요 라이브러리 설치

#깃 클론
!git clone https://github.com/SKTBrain/KoBERT.git
#KoBERT의 설치환경 설치
!pip install 'git+https://github.com/SKTBrain/KoBERT.git#egg=kobert_tokenizer&subdirectory=kobert_hf'

!pip install sentencepiece
!pip install transformers

import sentencepiece
import tensorflow as tf
import transformers 
from kobert_tokenizer import KoBERTTokenizer

5. 데이터 파이프라인 생성

#하이퍼파라미터
max_length = 128  # Maximum length of input sentence to the model.
batch_size = 32
epochs = 10

# Labels
labels = ["contradiction", "entailment", "neutral"]
class BertSemanticDataGenerator(tf.keras.utils.Sequence):
  '''Generate Batches of data

  Args: 
    sentence_pairs: premise와 hypothesis가 함께 있는 array
    labels: 원핫 인코딩 된 라벨값
    batch_size: 배치 사이즈 (integer)
    shuffle: True or False
    include_targets: 데이터에 라벨값 사용 여부 (True or False)

  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

    # BERT Tokenizer로 인코딩을 합니다.
    # SKT-brain KoBERT의 사전학습 모델 사용
    self.tokenizer = KoBERTTokenizer.from_pretrained('skt/kobert-base-v1')
    self.indexes = np.arange(len(self.sentence_pairs))
    self.on_epoch_end()

  def __len__(self):
    # 에폭별 배치수 지정
    return len(self.sentence_pairs) // self.batch_size
  
  def __getitem__(self, idx):
    #배치의 인덱스 
    indexes = self.indexes[idx * self.batch_size : (idx + 1) * self.batch_size]
    sentence_pairs = self.sentence_pairs[indexes]

	# batch_encode_plus로 두 문장을 인코딩하고, "sep"토큰으로 두 문장을 분리한다.
    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'
    )

    #인코딩된 결과를 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')

    #레이블값의 사용여부로 다른 결과값 추출
    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)

6. 모델 생성

from transformers import TFBertModel

#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'):
  #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.GRU(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']
  )

model.summary()

7. 모델 인풋 생성

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
)

8. 학습 진행 (Feature Extraction)

* 모델을 보시면 BERT 뒤에 LSTM layer과 더불어 레이블(contradiction, entailment, neutral)을 예측하기 위한 layer을 쌓았습니다.

따라서 4epoch 정도 먼저 학습을 진행하여 본인의 데이터에 대해 feature extraction을 진행해줍니다.

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

8-1. 학습진행 (Fine-Tuning)

Feature extraction을 마치고 이제 BERT 모델과 함께 학습을 하기 위해

bert_model.trainable = True

으로 변경해주고, optimizer를 수정하여 학습을 진행해줍니다.

#BERT모델과 함께 학습
bert_model.trainable = True

#모델에 수정사항이 있었으므로 다시 compile
model.compile(
    optimizer=tf.keras.optimizers.Adam(2e-5),
    loss='categorical_crossentropy',
    metrics=['accuracy']
)
model.summary()
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint

#checkpoint생성
mc = ModelCheckpoint('best_model.h5',monitor='val_accuracy',mode='max',verbose=1, save_best_only=True)

#학습
history = model.fit(
    train_data,
    validation_data=valid_data,
    epochs=20,
    use_multiprocessing=True,
    workers=-1,
    verbose=1,
    callbacks=mc
)

9. 예측

#test데이터의 premise와 hypothesis를 하나로 묶어줍니다.
sentence_pairs = test[['premise','hypothesis']].values.astype('str')

#test 데이터에 대해 모델 인풋 생성
test_data = BertSemanticDataGenerator(
        sentence_pairs, labels=None, batch_size=len(sentence_pairs), shuffle=False, include_targets=False,
    )

#load model
model  = tf.keras.models.load_model('best_model.h5', custom_objects={"TFBertModel": transformers.TFBertModel})

예측값 : 각 레이블의 대한 softmax 확률
pred = model.predict(test_data[0]).tolist()

#softmax 확률의 최고값을 추출하여 레이블 생성
idx = [np.argmax(x) for x in pred]
prediction = [labels[index] for index in idx]

test['label'] = prediction