이번 포스트에서는 🤗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에 배포되어 있는 상태이다.
데이터 셋의 경우 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%의 비율로 분리하였다.
본격적으로 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 참고!
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
))
Fine-tuning을 위해 tensorflow를 이용하는 것과 transformers의 TFTrainer를 활용하는 두 가지 방법이 존재하며 각각 아래 소개한다.
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 함수를 적용하였다.
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를 활용한 분산 학습이 가능하다.
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를 저장한다.
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_scores
를True
로 할 시 전체 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()
를 적용하였다.
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에는 조금 미치지 못하지만 오차범위 내로 성능 재현이 이루어졌다고 할 수 있을 것 같다.
HuggingFace transformers와 Tensorflow를 통해 사전학습모델을 Fine-tuning하는 방법 및 학습된 모델을 통해 Multi-class text classfication 문제까지 해결해보았다.😎
HuggingFace는 이제 NLP 분야에 있어 이젠 없어선 안될 정말 필수적인 도구가 되었다고 생각한다. 이젠 Google, OpenAI, Facebook등과 같은 기업처럼 엄청난 컴퓨팅 자원을 가지지 않더라도 누구나 HuggingFace에서 제공하는 모듈을 통해 대규모의 코퍼스로 학습된 모델을 활용할 수 있게 되었으니 말이다.
다음 포스팅에서는 HuggingFace를 좀더 스마트하게 사용할 수 있도록!
손쉽게 모델을 포팅하고 로드할 수 있는 언어 모델 저장소인 HuggingFace Model Hub와 inference의 속도를 x10이상 빠르게 해주는 Accelerated Inference API에 대해서도 간단히 알아볼 것이다.
작성글대로 몇 번씩이나 다시 진행했는데, 계속 trainer.train() 에서 막히네요 ㅠㅠ
TypeError: '>' not supported between instances of 'NoneType' and 'int'
이러한 에러가 뜨고,
self.args.eval_steps > 0
이 부분에서 self.args.eval 가 NoneType인 거 같은데, 문제가 무엇인지 알 수 있을까요?
데이터에서 결측치는 없었습니다!