[Basic NLP] Transformers와 Tensorflow를 활용한 BERT Fine-tuning

jaehyeong.an_·2021년 8월 6일
6

Basic NLP

목록 보기
3/6
post-thumbnail

이번 포스트에서는 🤗HuggingFace의 Transformers 라이브러리와 Tensorflow를 통해 사전 학습된 BERT모델을 Fine-tuning하여 Multi-Class Text Classification을 수행하는 방법에 대해 알아보고자 한다.

특히 이번에 fine-tuning을 진행할 모델은 최근에 KLUE를 통해 배포된 BERT모델이다.
KLUE(Korean Language Understanding Evaluation)는 최근 Upstage, NAVER, NYU, KAIST, Kakao 등 신뢰성 높은 기관 및 기업에서 한국어의 적은 데이터 셋 및 적절한 한국어 모델 평가지표가 없는 한계를 극복하고자 시작된 오픈 프로젝트이며 6월 15일에 1.0버전이 공개되었다.

📌 KLUE는 총 8가지 NLU task(Topic Classification, Semantic Textual, Natural Language Inference, Named Entity Recognition, Relation Extraction, Dependency parsing, Machine Reading Comprehension, Dialogue State Tracking)로 구성되어 있으며,
📌 Baseline 모델인 KLUE-BERT, KLUE-RoBERTa 모델이 HuggingFace Model Hub에 배포되어 있는 상태이다.


1. Load Dataset

데이터 셋의 경우 KLUE에서 Topic-Classification을 위해 사용한 YNAT 데이터 셋을 그대로 사용하여 성능을 재현해보기로 하였다. YNAT는 연합뉴스의 2016-202년까지의 뉴스 headline을 수집한 데이터 셋이며, 총 7가지 클래스(IT과학, 경제, 사회, 생활문화, 세계, 스포츠, 정치)로 분류되어있다.

Load Trainset

# Load Train-set
with open('_data/ynat-v1.1_train.json', mode='rt', encoding='utf-8-sig') as f:
    train_dataset = json.load(f)

train_dataset_list = [{'text':data['title'], 'label':data['label']} for data in train_dataset]
train_df = pd.DataFrame(train_dataset_list)
train_df.head()

Count by Label

train_df.groupby(by=['label']).count()


라벨별 갯수를 보면 총 7개의 클래스로 구분되어 있고, 어느정도 데이터셋의 balance가 맞춰져 있는 것을 알 수 있다. 🤹‍♂️

Label Encoding

from sklearn.preprocessing import LabelEncoder

label_encoder = LabelEncoder()
label_encoder.fit(train_df['label'])
num_labels = len(label_encoder.classes_)

train_df['encoded_label'] = np.asarray(label_encoder.transform(train_df['label']), dtype=np.int32)
train_df.head()


학습 시 Loss 계산을 위해 문자열 형태의 레이블을 숫자 형태의 카테고리로 인코딩을 수행한다. Encode를 위해 scikit-learn의 LabelEncoder를 사용하였으며, transform()메서드 수행시 encode, inverse_transform()메서드 수행시 decode를 수행한다.

Spliting data into training and validation set

train_texts = train_df["text"].to_list() # Features (not-tokenized yet)
train_labels = train_df["encoded_label"].to_list() # Labels

텍스트와 라벨을 따로 분리 후,

from sklearn.model_selection import train_test_split

# Split Train and Validation data
train_texts, val_texts, train_labels, val_labels = train_test_split(train_texts, train_labels, test_size=0.2, random_state=0)

모델 검증을 위해 validation set을 training set의 20%의 비율로 분리하였다.

2. Tokenizing the text

본격적으로 Tokenizing 및 pretrained 모델 사용을 위해 🤗HuggingFace의 Transformers라이브러리를 활용한다.
Transformers를 통해 저장된 모델은 기본적으로 pretrained model, tokenizer, vocab, config 파일 등을 포함하고 있으며, from_pretrained() 메소드를 통해 로드할 수 있다.

📌 지원 모델 및 세부 내용은 Transformers 공식 문서 참고

KLUE-BERT Model Path

HUGGINGFACE_MODEL_PATH = "klue/bert-base"

여기서 이용할 KLUE-BERT모델 또한 HuggingFace Model Hub에 배포되어 있으며 해당 모델 주소를 추후 from_pretrained()에 인자로 넣어주어 모델을 다운로드 및 로드할 수 있다.

Load Tokenizer and Tokenizing

from transformers import BertTokenizerFast

# Load Tokenizer
tokenizer = BertTokenizerFast.from_pretrained(HUGGINGFACE_MODEL_PATH)

# Tokenizing
train_encodings = tokenizer(train_texts, truncation=True, padding=True)
val_encodings = tokenizer(val_texts, truncation=True, padding=True)


Tokenizer는 BertTokenizer(), BertTokenizerFast() 무엇을 사용하던 상관없지만, BertTokenizerFast()BertTokenizer() 대비 1.2 ~ 1.5배 tokenizing 속도가 빠르다.

📌 BertTokenizerFast() 속도는 빠른 장점이 있지만, 성능에 영향을 줄 수 있으니 유의! 😅

truncation혹은 padding 옵션을 주어 input sequence의 길이를 맞춰줄 수 있으며, 여러가지 옵션으로 세세한 튜닝도 가능하다.

📌 Transformers 공식 문서 Tokenizer 참고!

3. Creating a Dataset object for TensorFlow

fine-tuning을 진행하기 전에 먼저 tokenized 된 데이터 셋을 Tensorflow의 Dataset object로 변환을 위해 from_tensor_slices()메서드를 수행한다.

import tensorflow as tf

# trainset-set
train_dataset = tf.data.Dataset.from_tensor_slices((
    dict(train_encodings),
    train_labels
))

# validation-set
val_dataset = tf.data.Dataset.from_tensor_slices((
    dict(val_encodings),
    val_labels
))

4. Fine-tuning BERT

Fine-tuning을 위해 tensorflow를 이용하는 것과 transformers의 TFTrainer를 활용하는 두 가지 방법이 존재하며 각각 아래 소개한다.

4.1 Using Native Tensorflow

Load Pretrained Model

from transformers import TFBertForSequenceClassification

num_labels = len(label_encoder.classes_)
model = TFBertForSequenceClassification.from_pretrained(HUGGINGFACE_MODEL_PATH, num_labels=num_labels, from_pt=True)

optimizer = tf.keras.optimizers.Adam(learning_rate=5e-5)
model.compile(optimizer=optimizer, loss=model.compute_loss, metrics=['accuracy'])

Text Classification 수행이 목적이므로 TFBertForSequenceClassification를 클래스를 활용한다.

위에서 정의한 HUGGINGFACE_MODEL_PATH를 다시 from_pretrained() 메소드에 전달하여 KLUE-BERT 모델을 다운로드 및 로드를 수행한다. 이때, num_labels에 라벨 갯수를 넘겨줄 수 있는데, 위에서 LabelEncoder객체에 .classes_메소드를 수행함으로써 라벨 갯수를 얻을 수 있다.

📌 참고로 KLUE에서 배포된 baseline 모델들은 Pytorch로 학습된 모델이기 때문에 Tensorflow에서 사용하기 위해서는 from_pretrained() 인자에 from_pt=True를 넣어줌으로써 Tensorflow모델로 변환 및 로드 할 수 있다.

Training

from tensorflow.keras.callbacks import EarlyStopping

callback_earlystop = EarlyStopping(
    monitor="val_accuracy", 
    min_delta=0.001, # the threshold that triggers the termination (acc should at least improve 0.001)
    patience=2)

model.fit(
    train_dataset.shuffle(1000).batch(16), epochs=5, batch_size=16,
    validation_data=val_dataset.shuffle(1000).batch(16),
    callbacks = [callback_earlystop]
)
Epoch 1/5
2284/2284 [==============================] - 380s 145ms/step - loss: 0.4405 - accuracy: 0.8550 - val_loss: 0.3587 - val_accuracy: 0.8806
Epoch 2/5
2284/2284 [==============================] - 328s 144ms/step - loss: 0.2827 - accuracy: 0.9015 - val_loss: 0.3989 - val_accuracy: 0.8684
Epoch 3/5
2284/2284 [==============================] - 328s 143ms/step - loss: 0.1977 - accuracy: 0.9333 - val_loss: 0.5046 - val_accuracy: 0.8691
Epoch 4/5
2284/2284 [==============================] - 328s 144ms/step - loss: 0.1395 - accuracy: 0.9528 - val_loss: 0.5165 - val_accuracy: 0.8654
Epoch 5/5
2284/2284 [==============================] - 327s 143ms/step - loss: 0.1041 - accuracy: 0.9664 - val_loss: 0.5962 - val_accuracy: 0.8683
<tensorflow.python.keras.callbacks.History at 0x7f00c4c65710>

training 방법은 tf.keras에서 모델을 훈련할 때와 같다. training 사전 종료를 위해 EarlyStopping callback 함수를 적용하였다.

4.2 Using TFTrainer class

from transformers import TFTrainer, TFTrainingArguments

training_args = TFTrainingArguments(
    output_dir='./results',          # output directory
    num_train_epochs=5,              # total number of training epochs
    per_device_train_batch_size=16,  # batch size per device during training
    per_device_eval_batch_size=64,   # batch size for evaluation
    warmup_steps=500,                # number of warmup steps for learning rate scheduler
    weight_decay=0.01,               # strength of weight decay
    logging_dir='./logs'            # directory for storing logs
)

with training_args.strategy.scope():
    trainer_model = TFBertForSequenceClassification.from_pretrained(huggingface_path, num_labels=num_labels, from_pt=True)

trainer = TFTrainer(
    model=trainer_model,                 # the instantiated 🤗 Transformers model to be trained
    args=training_args,                  # training arguments, defined above
    train_dataset=train_dataset,         # training dataset
    eval_dataset=val_dataset             # evaluation dataset
)

Training

trainer.train()

transformers의 Trainer API를 사용하면 보다 쉽게 model 학습이 가능하다. 또한, Trainer API는 기본적으로 멀티 GPU/TPU를 활용한 분산 학습이 가능하다.

5. Saving Model

transformers에서 제공하는 save_pretrained() 메소드를 사용하면 손쉽게 모델을 저장할 수 있다.
모델을 저장하게 되면 총 5가지의 파일이 저장 위치에 생성되며, 추후 해당 파일을 그대로 HuggingFace Model Hub로 포팅하여 손쉽게 로드할 수 있다.

Change id2label, label2id in model.config

id2labels = model.config.id2label
model.config.id2label = {id : label_encoder.inverse_transform([int(re.sub('LABEL_', '', label))])[0]  for id, label in id2labels.items()}

label2ids = model.config.label2id
model.config.label2id = {label_encoder.inverse_transform([int(re.sub('LABEL_', '', label))])[0] : id   for id, label in id2labels.items()}

학습된 모델은 기본적으로 모델 아키텍처, 레이어, label과 같은 모델 정보를 config 속성에 저장하게 된다. 이 config에는 id2label, label2id 라는 index값과 label속성이 매핑된 정보가 존재하는데, 우리가 위에서 LabelEncoder를 통해 label을 숫자형태로 encoding을 하여 학습하였기 때문에 해당 속성들 또한 encoding된 형태로 저장되어 있다. 그래서 이를 다시 decoding함으로써 본래의 label 값을 갖도록 변환한다.

Saving the model and tokenizer

MODEL_NAME = 'fine-tuned-klue-bert-base'
MODEL_SAVE_PATH = os.path.join("_model", MODEL_NAME) # change this to your preferred location

if os.path.exists(MODEL_SAVE_PATH):
    print(f"{MODEL_SAVE_PATH} -- Folder already exists \n")
else:
    os.makedirs(MODEL_SAVE_PATH, exist_ok=True)
    print(f"{MODEL_SAVE_PATH} -- Folder create complete \n")

# save tokenizer, model
model.save_pretrained(MODEL_SAVE_PATH)
tokenizer.save_pretrained(MODEL_SAVE_PATH)
_model/fine-tuned-klue-bert-base -- Folder create complete

('_model/fine-tuned-klue-bert-base/tokenizer_config.json',
 '_model/fine-tuned-klue-bert-base/special_tokens_map.json',
 '_model/fine-tuned-klue-bert-base/vocab.txt',
 '_model/fine-tuned-klue-bert-base/added_tokens.json',
 '_model/fine-tuned-klue-bert-base/tokenizer.json')

모델을 저장할 디렉토리를 지정한 후 save_pretrained() 메소드를 통해 model과 tokenizer를 저장한다.

6. Loading the saved Model and Prediction

Loading the model and tokenizer

from transformers import TextClassificationPipeline

# Load Fine-tuning model
loaded_tokenizer = BertTokenizerFast.from_pretrained(MODEL_SAVE_PATH)
loaded_model = TFBertForSequenceClassification.from_pretrained(MODEL_SAVE_PATH)

text_classifier = TextClassificationPipeline(
    tokenizer=loaded_tokenizer, 
    model=loaded_model, 
    framework='tf',
    return_all_scores=True
)

from_pretraiend() 메소드에 저장된 모델 디렉토리를 넣어줌으로써 우리가 fine-tuning한 model 및 tokenizer를 로드할 수 있다.

추가로 transformers의 Pipelines 클래스를 사용하면 손쉽게 특정 task에 대한 inference를 수행할 수 있다. 우리가 하고자 하는 것은 분류문제이기 때문에 TextClassificationPipeline 클래스를 사용하였다.

framework에는 학습된 모델의 framework를 넣어준다. (tensorflow='tf', pytorch='pt')
return_all_scoresTrue로 할 시 전체 label에 대한 확률값을 제공한다.

Load Testset
이제 모델의 성능을 평가하기 위해 Test용 데이터셋을 로드한다.

# Load Test-set
with open('_data/ynat-v1.1_dev.json', mode='rt', encoding='utf-8-sig') as f:
    test_dataset = json.load(f)

test_dataset_list = [{'text':clean_text(data['title']), 'label':data['label']} for data in test_dataset]
test_df = pd.DataFrame(test_dataset_list)
test_df.head()

Prediction using Pipelines

predicted_label_list = []
predicted_score_list = []

for text in test_df['text']:
    # predict
    preds_list = text_classifier(text)[0]

    sorted_preds_list = sorted(preds_list, key=lambda x: x['score'], reverse=True)
    predicted_label_list.append(sorted_preds_list[0]) # label
    predicted_score_list.append(sorted_preds_list[1]) # score
test_df['pred'] = predicted_label_list
test_df['score'] = predicted_score_list
test_df.head()


label과 score를 각각 담을 리스트를 준비한 후 , 위에서 정의한 text_classifier라는 이름의 TextClassificationPipeline 클래스에 텍스트를 전달한다.

TextClassificationPipeline 클래스는 {'label':'example', 'score':0.9} 포맷으로 결과값을 return하며, return_all_scores=True를 지정하였기에 score가 높은 순서로 label을 정렬하기 위해 sorted()를 적용하였다.

7. Evaluation

from sklearn.metrics import classification_report

print(classification_report(y_true=test_df['label'], y_pred=test_df['pred']))
              precision    recall  f1-score   support

        IT과학       0.74      0.82      0.78       554
          경제       0.81      0.83      0.82      1348
          사회       0.88      0.80      0.84      3701
        생활문화       0.81      0.84      0.82      1369
          세계       0.83      0.84      0.84       835
         스포츠       0.74      0.98      0.85       578
          정치       0.80      0.84      0.82       722

    accuracy                           0.83      9107
   macro avg       0.80      0.85      0.82      9107
weighted avg       0.83      0.83      0.83      9107

scikit-learn의 classification_report를 통해 label별 결과를 확인한다. f1-score가 0.83으로 기존 KLUE-BERT-BASE의 Topic Classification 점수인 85.49에는 조금 미치지 못하지만 오차범위 내로 성능 재현이 이루어졌다고 할 수 있을 것 같다.


Outro

HuggingFace transformers와 Tensorflow를 통해 사전학습모델을 Fine-tuning하는 방법 및 학습된 모델을 통해 Multi-class text classfication 문제까지 해결해보았다.😎
HuggingFace는 이제 NLP 분야에 있어 이젠 없어선 안될 정말 필수적인 도구가 되었다고 생각한다. 이젠 Google, OpenAI, Facebook등과 같은 기업처럼 엄청난 컴퓨팅 자원을 가지지 않더라도 누구나 HuggingFace에서 제공하는 모듈을 통해 대규모의 코퍼스로 학습된 모델을 활용할 수 있게 되었으니 말이다.

다음 포스팅에서는 HuggingFace를 좀더 스마트하게 사용할 수 있도록!
손쉽게 모델을 포팅하고 로드할 수 있는 언어 모델 저장소인 HuggingFace Model Hub와 inference의 속도를 x10이상 빠르게 해주는 Accelerated Inference API에 대해서도 간단히 알아볼 것이다.

References

profile
🌒 Don't be a knew-it-all, Be a Learn-it-all

1개의 댓글

comment-user-thumbnail
2022년 7월 15일

작성글대로 몇 번씩이나 다시 진행했는데, 계속 trainer.train() 에서 막히네요 ㅠㅠ
TypeError: '>' not supported between instances of 'NoneType' and 'int'
이러한 에러가 뜨고,
self.args.eval_steps > 0
이 부분에서 self.args.eval 가 NoneType인 거 같은데, 문제가 무엇인지 알 수 있을까요?
데이터에서 결측치는 없었습니다!

답글 달기