[이론/Imple] NLP 전이학습모델 (BERT 감성분석모델 Imple)
Transfer Learning
전이학습이란 이미 훈련되어있는 모델을 가져다 사용하는 걸 말함.
1) Feature-based Transfer Learning : 미리 훈련된 모델의 출력 feature를 새로운 모델의 입력으로 가져다 사용.
2) Fine-Tuning : 미리 훈련된 weight를 초기값으로 사용해 동일한 모델을 미세조정.
Fine-Tuning은 last layer를 추가해주면 됨(지도학습이므로 역전파가 일어남)
Fine-Tuning의 장점은 BERT에는 이미 많은 것이 인코딩 되어있어서 더 빠르게 개발할 수 있고, 필요한 데이터가 감소되고, 높은 결과치를 얻을 수 있다. 그러나 사이즈가 크고 (작은 모델이 필요해도 범용적인 BERT를 가져다 쓰게 되므로), fine-tuning의 속도가 느리고, inferencing의 속도가 느리고, 특정 domain에 특화된 단어는 모른다는 단점이 있다.
전이학습 모델 비교
* ELMO (Embeddings from Language Model)
Bidirectional LSTM을 이용해 양방향으로 문맥 전체를 파악, 순차적인 진행
각각의 방향에 대해 autoregressive 한 language model
현재는 사용X
* OpenAI GPT (Generative Pre-Training Transformer)
Transformer의 decoder 12개를 쌓아서 구성
decoder를 사용하므로 이전 단어들 만으로 다음 단어를 예측
(forward language model, autogressive 자기회귀적인 모델)
* BERT (Bidirectional Encoder Representatinal from Transfromers)
Transformer의 encoder 12/24개를 쌓아서 구성
MLM(Masked Language Model), Next Sentence Prediction으로 훈련
* GPT-2 (GPT와 기본구조 동일)
* XLNet (Transformer-XL)
Permutation Language Modeling (PLM) 방식으로 훈련
XLNet의 크기는 BERT-Large와 같은 24-Layer, 340M parameters
BERT (Bidirectional Encoder Representations from Transformers)
2018년에 발표된 모델로 104개 국어를 동시 발표했다.
ALBert, RoBERTA, TinyBERT, DistilBERT, SpanBERT 등 다양한 변형모델들이 발생했다.
질의 응답 시스템이나 감성분석 등의 다양한 GLUE task에서 SOTA를 달성했다.
Pre-trained token embedding(WordPiece)을 사용
=> 3만개의 영문, 11만개의 subwords 바탕
BERT-Base model : Transformer layer 12개, Total parameters 110M
BERT-Large model : Transformer layer 24개, Total parameters 340M
BERT의 기본 접근 방식
1) 범용모델
2) scalable 한 형태로 구현
3) 많은 머신 리소스로 훈련해 성능을 높임
4) 11개의 NLP과제에 하나의 pre-trained 모델을 공통적으로 사용 (각각의 과제에 대해 약간의 미세조정 적용)
https://arxiv.org/abs/1810.04805?source=post_page
BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
We introduce a new language representation model called BERT, which stands for Bidirectional Encoder Representations from Transformers. Unlike recent language representation models, BERT is designed to pre-train deep bidirectional representations from unla
arxiv.org
BERT Pre-training은 비지도학습으로 (엄밀히 말하면 알고리즘 사람이 레이블을 직접 라벨링만 안할 뿐 알고리즘 자체는 지도학습 알고리즘이라고 함) 대규모 corpus를 사용하고, 대규모 resource를 사용한다.
BERT Fine-Tuning은 다양한 downstream NLP task에 적용한다.
BERT가 모든 NLP문제에 적용되지는 않는다. 인코더쪽이므로
분류(Classification), 객체명 인식(NER), 품사태깅(POS Tagging), 질의응답분야에는 뛰어나지만
언어모델로 다음 단어를 예측하거나, text generation, 번역분야에는 적용하지 않는다.
GLUE Task
성능평가에서 사용하는 GLUE Benchmark(GLUE Task)는
- GLUE(General Language Understanding Evaluation)는 한 개의 자연어 처리 모델에 대해 여러 태스크들을 훈련시키고, 그 성능을 평가 및 비교 분석하기 위한 데이터셋들로 구성되어있다.
- 모델들의 자연어 이해 능력을 평가하기 위해 다양하고 해결하기 어려운 9개의 Task Dataset으로 구성되어있다.
- BERT와 같은 전이학습 모델들을 평가하기 위한 필수적인 벤치마크이다.
GLUE Task 구성을 살펴보면,
1) CoLA (The Corpus of Linguistic Acceptability) : 문장이 문법적으로 맞는지 분류(이진분류)
2) SST-2 (The Stanford Sentiment Treebank) : 영화평 감성 분류(positive, negative, neutral)
3) MRPC (Microsoft Research Paraphrase Corpus) : 문장 B가 문장 A의 다른 표현(paraphrase)인지 여부(이진분류)
4) STS-B (The Semantic Textual Similarity Benchmark) : 문장 A와 B는 얼마나 유사한가
5) QQP (Quora Question Paris) : 두개의 질문이 서로 유사한가(이진분류)
6) MNLI (Multi-Gener Natural Language Inference) : 문장 B가 문장 A에 이어지는지,반대되는지, 무관한지 여부,
문장 A (Hypothesis) [seq] 문장 B (Premise)
7) QNLI (Question Natural Language Inference) : 문장 B가 문장 A의 질문에 대한 답을 포함하는지 여부(이진분류)
8) RTE (Recognizing Textual Entailment) : MNLI와 유사하나, 상대적으로 훨씬 적은 데이터셋(이진분류)
9) WNLI (Winograd NLI) : 문장 B가 문장 A의 애매한 대명사를 정확한 명사로 대체하였나 (이진분류)
기타 non-GLUE : SQuAD(Stanford NLP Group, Wikipedia 질의응답)
BERT 구성,훈련방법
BERT는 12/24 encoders 구성을 가진다.
모든 input 데이터는 [CLS] 토큰으로 시작
Sequence A [SEP] Sequence B 식으로 input이 두개면 시퀀스 토큰이 삽입됨
Masked Language Modeling(MLM)과 Next Sentence Prediction 두 가지 task를 동시에 학습
C ([CLS])는 모든 자연어 분류문제에 prediction으로 사용
Masked Language Modeling (MLM)
: input에 random하게 몇 개의 토큰을 masking
ex) input label : the man went to the [MASK] to buy a [MASK] of milk
store gallon
: left, right context만을 가지고 maked word의 원래 단어를 예측
=> 전체 학습 데이터 token의 15%를 masking,
이중 80%는 [MASK] token 으로 replace
10%는 random word로 replace
10%는 unchanged (tagging만 바뀐것으로 표시)
BERT loss function은 masked value의 prediction만 고려하고 non-masked word는 무시한다.
Encoder output 상단에 softmax classification layer를 추가해 Masked word의 확률분포를 예측한다.
Next Sentence Prediction
Training data로 sentence pair를 사용
주요목적은 output의 C([CLS])를 train 시키는 것
ex) 100,000개의 sentence가 있고 BERT language model 을 pre-train 시키려면 50,000개 pair의 training data 존재
50% pair는 second sentence가 실제 다음 sentence(corpus에서 연속된 문장을 뽑아냄)
나머지 50% pair는 second sentence를 corpus 에서 random하게 선택(사람이 하는거 아님)
첫번째 label은 'IsNext' (첫번째 문장과 다음 문장 관련O), 두번째 label은 'NotNext'(관련X)
이어지는 문장이 앞선문장과 연결되었는지 훈련
1) 전체 input sequence를 Transformer model에 입력한다.
2) [CLS] 토큰의 output을 simple classification layer를 이용해 2*1 shaped vector로 변환한다.
3) softmax를 이용해 IsNextSequence를 예측한다.
*BERT 모델 훈련 시 MLM과 Next Sentece Prediction은 동시에 훈련 되고, 목적은 combined loss function을 mininize 하는 것이다
Fine-tuning BERT
BERT는 기본적으로 두 문장을 입력으로 취할 수 있도록 설계되어있다.
사전 훈련에 사용되었던 분류층을 제거하고, 이를 GLUE task를 수행하기 위한 레이어(task-specific layer)로 변경해준다.
=> 대부분의 경우 변경해주는 레이어가 이진분류계층이다
위 과정을 거쳐 변형된 모델을 n번의 epoch 동안 재학습한다 => Fine-Tuning
이때 사전학습 모델에서 차용한 모델 중간부는 자연어를 잘 '이해'하는 파라미터를 지니고 있기 때문에 문제 해결에 큰 도움이 됨
소스 작성
기초적인 전이학습 모델은 Tensorflo hub 에서 가져올 수 있다.
https://www.tensorflow.org/hub?hl=ko
TensorFlow Hub
TensorFlow Hub는 기계 학습 모델의 재사용 가능한 부분을 게시, 검색 및 사용하기 위한 라이브러리입니다.
www.tensorflow.org
Naver movie dataset을 이용해 사전훈련된 BERT 모델을 fine tuning을 진행해보겠다.
실행환경은 구글코랩 GPU 스탠다드를 사용하였다.
진행 순서
1) 데이터셋 불러오기
2) TensorFlow Hub에서 BERT 모델 로드
3) BERT와 분류기를 결합하여 나만의 모델 구축
4) BERT를 미세 조정하여 자신만의 모델을 훈련
5) 모델을 저장하고 문장 분류에 사용
먼저 필요한 라이브러리 설치
!pip install -q -U tensorflow-text
!pip install -q tf-models-official
사용할 라이브러리 임포트
import os
import shutil
import tensorflow as tf
import tensorflow_hub as hub
import tensorflow_text as text
from official.nlp import optimization # to create AdamW optimizer
import matplotlib.pyp
구글 드라이브에 미리 마운트 해둔 데이터 사용
DATA_TRAIN_PATH = tf.keras.utils.get_file("학습파일이름.txt","경로")
DATA_TEST_PATH = tf.keras.utils.get_file("테스트파일이름.txt" ,"경로")
데이터 확인
train_data = pd.read_csv(DATA_TRAIN_PATH, delimiter='\t')
test_data = pd.read_csv(DATA_TEST_PATH, delimiter='\t')
print(train_data.shape)
print(test_data.shape)
(150000, 3) (50000, 3) 개수가 확인되는데 BERT 모델이 워낙 커서 시간이 오래걸리므로 사용할때 원하는만큼 데이터를 sample 해서 개수를 조정하면 된다.
여기서는 10분의 1만 사용하고자 한다.
train_data = train_data.sample(n=15000, random_state=1)
test_data = test_data.sample(n=5000, random_state=1)
train_data.dropna(inplace=True)
test_data.dropna(inplace=True)
print(train_data.shape)
print(test_data.shape)
train_data.head(3)
클래스 이름 생성
class_names = {1: '긍정', 0: '부정'}
GPU 성능 최적화 옵션 적용, 배치사이즈 적용
입력 데이터를 텐서형으로 변환 (numpy는 cpu에서 돌고, gpu에서는 텐서로 해야함)
AUTOTUNE = tf.data.AUTOTUNE #자동으로 GPU 성능 최적화
batch_size = 32
#tf.data.Dataset.from_tensor_slices : 미리 데이터를 텐서형으로 변환
train_ds = tf.data.Dataset.from_tensor_slices((train_data['document'], train_data['label']))
train_ds = train_ds.batch(batch_size).shuffle(10000).cache().prefetch(buffer_size=AUTOTUNE)
test_ds = tf.data.Dataset.from_tensor_slices((test_data['document'], test_data['label']))
test_ds = test_ds.batch(batch_size).shuffle(10000).cache().prefetch(buffer_size=AUTOTUNE)
데이터 확인
#실제 데이터 확인
for text_batch, label_batch in train_ds.take(1):
for i in range(6): #여섯개만 확인해보기
print(f'영화평: {text_batch.numpy()[i].decode("utf-8")}')
label = label_batch.numpy()[i]
print(f'Label : {label} ({class_names[label]})')
다음으로 TensorFlow Hub에서 모델을 로드한다.
Tensorflow hub bert모델 튜토리얼을 참고하면 될 듯하다.
bert_glue.ipynb
Run, share, and edit Python notebooks
colab.research.google.com
BERT 모델이 사용할 사전 처리의 3가지 출력( input_words_id , input_mask 및 input_type_id)값을 미리 숙지하고
전처리 모델을 불러온다
# 전처리 모델 불러오기
bert_preprocess_model = hub.KerasLayer(tfhub_handle_preprocess)
일부 텍스트에 대한 전처리 모델을 시도하고 출력을 확인한다.
text_test = ['여태까지 영화관에서 본영화중에 제일 재미있었음.']
text_preprocessed = bert_preprocess_model(text_test)
print(f'Keys : {list(text_preprocessed.keys())}')
print(f'Shape : {text_preprocessed["input_word_ids"].shape}')
print(f'Word Ids : {text_preprocessed["input_word_ids"]}') # token id
print(f'Input Mask : {text_preprocessed["input_mask"]}') # padding 여부
print(f'Type Ids : {text_preprocessed["input_type_ids"]}') #1st, 2nd sentence 구분
다음으로 BERT 모델을 사용한다.
BERT 모델은 pooled_output , sequence_output , encoder_outputs 의 3가지 중요한 키가 있는 맵을 반환한다.
- pooled_output : 각 입력 시퀀스를 전체적으로 나타냄, shape는 [batch_size, H] => 전체 영화 리뷰에 대한 임베딩.
- sequence_output 은 컨텍스트의 각 입력 토큰을 나타냄. shape은 [batch_size, seq_length, H] => 영화 리뷰의 모든 토큰에 대한 컨텍스트 임베딩.
- encoder_outputs : 'L'개의 Transformer 블록의 중간 activation. outputs["encoder_outputs"][i] 는 0 <= i < L 에 대해 i 번째 Transformer 블록의 출력이 있는 [batch_size, seq_length, 1024] 모양의 텐서로 list의 마지막 값은 sequence_output 과 같음.
=>fine-tuning을 위해 pooled_output array를 사용.
인코더 모델 불러오기
# encoder 모델 불러오기
bert_model = hub.KerasLayer(tfhub_handle_encoder)
BERT 모델 확인해보기
bert_results = bert_model(text_preprocessed)
print(f'load된 BERT 모델: {tfhub_handle_encoder}')
print(f'Pooled Outputs Shape:{bert_results["pooled_output"].shape}')
print(f'Pooled Outputs Values:{bert_results["pooled_output"][0, :10]}')
print()
print(f'Sequence Outputs Shape:{bert_results["sequence_output"].shape}')
print(f'Encoder Outputs Shape:{len(bert_results["encoder_outputs"])}')
실제로 모델을 정의 한다 (fine tuning 시작)
전처리 모델, 선택된 BERT 모델과 하나의 Dense 및 Dropout 레이어를 사용해 모델 생성
def build_classifier_model():
text input = tf keras layers Input(shape=() dtype=tf string name='text')
#전처리 model
preprocessing_layer = hub.KerasLayer(tfhub_handle_preprocess, name='preprocessing')
encoder_inputs = preprocessing_layer(text_input)
#pretrained BERT model
encoder = hub.KerasLayer(tfhub_handle_encoder, trainable=True, name='BERT_encoder')
outputs = encoder(encoder_inputs)
#last layer 추가
net = outputs['pooled_output']
net = tf.keras.layers.Dropout(0.1)(net)
#activation=None : 여기서 시그모이드로 주지않고 나중에 외부에서 시그모이드 처리
net = tf.keras.layers.Dense(1, activation=None, name='classifier')(net)
return tf.keras.Model(text_input, net)
모델 구조 확인
tf.keras.utils.plot_model(classifier_model)
모델학습과정 에서 손실함수를 적용한다
이진 분류 문제이고 모델이 확률을 출력하므로 'losses.BinaryCrossentropy' 손실 함수를 사용
loss = tf.keras.losses.BinaryCrossentropy(from_logits=True)
metrics = tf.metrics.BinaryAccuracy()
옵티마이저 진행
Adam 중 adamw 옵티마이저 타입 적용
epochs = 5
steps_per_epoch = len(train_ds)
num_train_steps = steps_per_epoch * epochs
num_warmup_steps = int(0.1*num_train_steps)
init_lr = 3e-5
optimizer = optimization.create_optimizer(init_lr=init_lr,
num_train_steps=num_train_steps,
num_warmup_steps=num_warmup_steps
optimizer_type='adamw')
classifier_model.compile(optimizer=optimizer,
loss=loss,
metrics=metrics)
모델 훈련 (약 30분)
import time
s = time.time()
print(f'Training model with {tfhub_handle_encoder}\n')
history = classifier_model.fit(x=train_ds,
validation_data=test_ds,
epochs=epochs)
모델 평가
loss, accuracy = classifier_model.evaluate(test_ds)
print(f'Loss: {loss}')
print(f'Accuracy: {accuracy}')
시간 경과에 따른 정확도, 손실 시각화
history_dict = history.history
print(history_dict.keys())
acc = history_dict['binary_accuracy']
val_acc = history_dict['val_binary_accuracy']
loss = history_dict['loss']
val_loss = history_dict['val_loss']
epochs = range(1, len(acc) + 1)
fig = plt.figure(figsize=(10, 6))
fig.tight_layout()
plt.subplot(2, 1, 1)
plt.plot(epochs, loss, 'r', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.subplot(2, 1, 2)
plt.plot(epochs, acc, 'r', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')
여기까지 완료되면 튜닝한 모델을 저장한다
dataset_name = 'kor_movie'
saved_model_path = '/{}_bert'.format(dataset_name.replace('/','_'))
classifier_model.save(saved_model_path, include_optimizer=False)
저장한 모델을 reload 하는 방법
reloaded_model = tf.saved_model.load(saved_model_path)
리로드한 모델에 테스트 데이터 넣어보기
def print_my_examples(inputs, results):
result_for_printing = \
[f'input:{inputs[i]:<30} : score : {results[i][0]:.6f}'
for i in range(len(inputs))]
print(*result_for_printing, sep='\n')
print()
examples=[
text_test[0],
'이것은 놀라운 영화입니다!',
'영화는 너무 훌륭했다!',
'영화는 밋밋했다',
'영화는 재미있었다.',
'영화는 끔찍했다...',
]
#여기서 시그모이드 처리
reloaded_results = tf.sigmoid(reloaded_model(tf.constant(examples)))
print_my_examples(examples, reloaded_results)