한국어 사전학습 ELECTRA + 오픈소스 데이터로 돈안드는 긍부정 분류 모델 만들기
- 본 포스트에서는 한국어 댓글로 사전학습된 ELECTRA 모델 kcELECTRA로 파인튜닝하여 긍부정 분류모델을 구축하였습니다.
- 데이터는 네이버 쇼핑, 스팀에서 수집한 오픈소스 데이터를 이용하였으며, 레퍼런스에 출처를 기재해두었습니다.
- 또한, 본 업로드는 ipynb 파일로 작성한 내용을 업로드 하였는데, 방법이 궁금하신 분들은 아래 링크 참고하시면 되겠습니다.
https://woni-log.tistory.com/6
from IPython.core.display import display, HTML
display(HTML("<style>.container {width:90% !important;}</style>"))
# 최신버전설치
!pip install transformers[torch]
시드 고정¶
- 모델의 재현성(reproductiblity)을 위해 시드를 고정한다.
# fix random seed
## reproductiblity
import random
import numpy as np
import torch
import os
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
random_seed = 42
def seed_everything(seed=42):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.benchmark = False
torch.use_deterministic_algorithms(True)
os.environ["PYTHONHASHSEED"] = str(seed)
os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0"
seed_everything(random_seed)
데이터 불러오기¶
- 데이터셋 : https://github.com/bab2min/corpus/tree/master/sentiment
- 데이터는 네이버 쇼핑리뷰 데이터 20만건과 스팀 리뷰 데이터 10만건을 함께 사용
파인튜닝용 학습 데이터는 나의 도메인에 맞는 데이터만 사용하는 것이 좋은가?
- 실험 결과, 여러 도메인의 데이터를 동시에 사용한다고 하더라도, 나의 도메인 데이터가 포함되어 있기만 하면 모델의 성능이 줄어들지 않고, 오히려 증가하는 경우가 있다. 참고 : https://bab2min.tistory.com/657
데이터 전처리 : 네이버 리뷰의 경우 별점이 1, 2, 4, 5로 구성되어 있고, 1점 2점인 경우 부정으로, 4점 5점인 경우 긍정으로 입력
- 부정 : 0, 긍정 : 1
!git clone https://github.com/bab2min/corpus.git
import pandas as pd
# 네이버 쇼핑 리뷰
naver_shopping = pd.read_csv('corpus/sentiment/naver_shopping.txt', sep='\t', header=None, names=['sentiment', 'text'])
naver_shopping
naver_shopping['sentiment'] = naver_shopping['sentiment'].apply(lambda x: 1 if x > 3 else 0)
naver_shopping
steam = pd.read_csv('corpus/sentiment/steam.txt', sep='\t', header=None, names=['sentiment', 'text'])
steam
data = pd.concat(naver_shopping, steam)
데이터 스플릿¶
- 학습 때 사용할 train, val 데이터와,
- 테스트할 test 데이터로 나눔
Data split¶
- 학습을 위한 train set, 최종 평가를 위한 test set, hyper parameter tuning을 위한 validation set으로 스플릿
- train data에서 5%만 떼서 validation용으로 사용
# data split
from sklearn.model_selection import train_test_split
x_train, x_val, y_train, y_val = train_test_split(data['text'], data['sentiment'], stratify = data['sentiment'], random_state=random_seed, test_size=0.1)
x_val, y_val, x_test, y_test = train_test_split(x_val, y_val, stratify=y_val, random_state=random_seed, test_size=0.5)
x_train.reset_index(drop=True, inplace=True)
x_val.reset_index(drop=True, inplace=True)
y_train.reset_index(drop=True, inplace=True)
y_val.reset_index(drop=True, inplace=True)
데이터 전처리¶
- 학습된 kcELECTRA Tokenizer 불러오기
- truncation 과 padding을 통해 임베딩 벡터의 차원을 일치시킴
*Tokenizer란?
- 트랜스포머 모델은 원래의 문자열을 그대로 입력을 받지 못한다. 따라서 적절한 숫자로 변환해주는 과정이 필요하다.
- 이때 문자단위로 각 character하나 당 하나의 숫자로 배핑하는 문자토큰화, 단어 하나 당 하나의 숫자로 매핑하는 단어 토큰화가 있다.
- 그러나 이 둘은 단점이 많아, 상호 보완한 부분단어 토큰화(subword tokenizing)을 주로 사용한다.
- 자주 등장하는 특정 단어들은 다른 고유한 token으로 치환한다. pretrain을 통해 학습된다.
- 예를 들어 "Tokenizing text is a core task of NLP."를 DistilBertTokenizer로 토큰화 하면 ['[CLS]', 'token', '##izing', 'text', 'is', 'a', 'core', 'task', 'of', 'nl', '##p', '.', '[SEP]']과 같이 반환된다.
- [CLS], [SEP]은 각각 문장의 시작과 끝을 나타낸다.
from transformers import AutoTokenizer
model_name = "beomi/KcELECTRA-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 모델의 인풋값은 텍스트가 아닌 tokenizer를 통해 인코딩된 값이 들어가므로, 인코딩을 미리 진행함
train_encoding = tokenizer(x_train.values.tolist(), truncation=True, padding=True)
val_encoding = tokenizer(x_val.values.tolist(), truncation=True, padding=True)
import torch
class SentimentDataset(torch.utils.data.Dataset):
def __init__(self, encodings, labels):
self.encodings = encodings
self.labels = labels
def __getitem__(self, idx):
item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
item['labels'] = torch.tensor(self.labels[idx])
return item
def __len__(self):
return len(self.labels)
train_dataset = SentimentDataset(train_encoding, y_train)
val_dataset = SentimentDataset(val_encoding, y_val)
Finetuning¶
- training argument 설정
모델의 구조¶
- 사용한 모델은 사전훈련된 모델이다.
- 자연어 처리 모델에서 사전 훈련은 보통, 다음단어를 예측(BERT 계열의 경우 MASKING된 단어의 예측)을 잘 수행할 수 있도록 사전학습한다.
이렇게 사전학습을 진행하는 이유는, Label이 필요없고, 데이터 자체만으로 스스로 정답을 가지고 있는 Self-Supervised Learning 형태로 진행되기 때문에 매우 대량의 데이터를 입력할 수 있다.
사전학습이 진행된 이후에는 위의 다음단어를 예측(정확히는 다음단어를 분류)하는 부분을 떼어내고, 내가 수행하고자 하는 task에 맞게 교체한다.
사전학습된 부분을 모델의 Body라고 하고, 예측을 수행하는 부분을 Prediction Head라고 한다.
- 파인튜닝에서는 이러한 Prediction Head를 교체하며, 기존 weight으로 initialize하기에 보다 적은 양의 데이터로 보다 빠르게 학습을 수행할 수 있다.
혹시 이진 분류 문제가 아닌 더 많은 레이블을 분류하는 문제라면, AutoModelForSequenceClassification.from_pretrained의 인자로 num_labels={레이블 개수}를 주면된다
TrainingArguments¶
- 수정이 필요한 argument 목록
- output_dir : 데이터가 저장될 폴더
- num_train_epochs : 전체 데이터를 몇번 노출시켜 학습을 진행할 것인지
- per_device_train_batch_size : gpu 램 용량을 보면서 50%이하로 사용하면 늘리고, Out of Memory에러가 발생하면 줄임 (2^n 단위로, 32 -> 64 -> 128)
- batch_size를 작게 훈련해야 flat minima에 도달되어 generalization이 더 잘된다는 논문도 있으나, 학습을 빠르게 진행하기 위해 우선 가능한 큰 batch size를 이용
- logging_dir : 로그 데이터를 저장할 폴더 입력(추후 tensorboard에서활용)
# 디버깅용
import os
os.environ['CUDA_LAUNCH_BLOCKING']= '1'
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
from transformers import AutoModelForSequenceClassification, Trainer, TrainingArguments
from transformers import EarlyStoppingCallback
model_name = "beomi/KcELECTRA-base"
training_args = TrainingArguments(
output_dir='./result/20240206', # output directory
num_train_epochs=2, # total number of training epochs
per_device_train_batch_size=128, # batch size per device during training
per_device_eval_batch_size=32, # 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='./result/20240206', # directory for storing logs
logging_steps=10,
# fp16=True,
lr_scheduler_type='cosine_with_restarts',
save_strategy='steps',
load_best_model_at_end=True,
evaluation_strategy='steps',
optim='adamw_torch',
learning_rate = 5e-6,
report_to="tensorboard"
)
model = AutoModelForSequenceClassification.from_pretrained(model_name)
trainer = Trainer(
model=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
callbacks = [EarlyStoppingCallback(early_stopping_patience=5)]
)
trainer.train()
# 모델 저장
trainer.save_model("./result/20240206/models")
Evaluate¶
- 모델의 아웃풋 값은 logit이다.
- 즉 이는 마지막 노드의 출력값이고, output layer에 들어가기 전 값이다.
- sigmoid에 넣어주어 최종 아웃풋 값을 반환받는다
# 토크나이저 불러오기
from transformers import AutoTokenizer
model_name = "beomi/KcELECTRA-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)
# fine tuning한 모델 불러오기
from transformers import AutoModel, AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained('./result/20240206/models')
import torch
import numpy as np
sigmoid = torch.nn.Sigmoid()
def predict(text: str) -> str:
encoding = tokenizer(text, return_tensors="pt")
encoding = {k: v.to(model.device) for k,v in encoding.items()}
outputs = model(**encoding)
probs = sigmoid(outputs.logits.squeeze().cpu())
if probs[1] >= 0.5:
return 1
else:
return 0
import pandas as pd
# 위에서 스플릿한 테스트 데이터 외에, 실제 추론에 사용할 데이터를 가져와서 테스트하였다.(학습때 노출 x)
test = pd.read_csv('./result/test_result.csv', index_col=0)
test[['text', 'sentiment']]
pred = test['text'].apply(predict)
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
cm = confusion_matrix(test['sentiment'], pred, normalize='true')
disp = ConfusionMatrixDisplay(cm, display_labels=['negative', 'positive'])
disp.plot(cmap='Blues')