데이터/Machine Learning

한국어 사전학습 ELECTRA + 오픈소스 데이터로 돈안드는 긍부정 분류 모델 만들기

성장하기 2024. 2. 18. 21:41

 

 

- 본 포스트에서는 한국어 댓글로 사전학습된 ELECTRA 모델 kcELECTRA로 파인튜닝하여 긍부정 분류모델을 구축하였습니다.

- 데이터는 네이버 쇼핑, 스팀에서 수집한 오픈소스 데이터를 이용하였으며, 레퍼런스에 출처를 기재해두었습니다.

- 또한, 본 업로드는 ipynb 파일로 작성한 내용을 업로드 하였는데, 방법이 궁금하신 분들은 아래 링크 참고하시면 되겠습니다.

https://woni-log.tistory.com/6

 

 

kcELECTRA_blogging
In [6]:
from IPython.core.display import display, HTML
display(HTML("<style>.container {width:90% !important;}</style>"))
/var/folders/wv/bnpq1s_d23qg2x2m69xg48gc0000gn/T/ipykernel_5937/3510566465.py:1: DeprecationWarning: Importing display from IPython.core.display is deprecated since IPython 7.14, please import from IPython display
  from IPython.core.display import display, HTML
In [ ]:
# 최신버전설치
!pip install transformers[torch]
Requirement already satisfied: transformers[torch] in /usr/local/lib/python3.10/dist-packages (4.35.2)
Requirement already satisfied: filelock in /usr/local/lib/python3.10/dist-packages (from transformers[torch]) (3.13.1)
Requirement already satisfied: huggingface-hub<1.0,>=0.16.4 in /usr/local/lib/python3.10/dist-packages (from transformers[torch]) (0.20.3)
Requirement already satisfied: numpy>=1.17 in /usr/local/lib/python3.10/dist-packages (from transformers[torch]) (1.23.5)
Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.10/dist-packages (from transformers[torch]) (23.2)
Requirement already satisfied: pyyaml>=5.1 in /usr/local/lib/python3.10/dist-packages (from transformers[torch]) (6.0.1)
Requirement already satisfied: regex!=2019.12.17 in /usr/local/lib/python3.10/dist-packages (from transformers[torch]) (2023.12.25)
Requirement already satisfied: requests in /usr/local/lib/python3.10/dist-packages (from transformers[torch]) (2.31.0)
Requirement already satisfied: tokenizers<0.19,>=0.14 in /usr/local/lib/python3.10/dist-packages (from transformers[torch]) (0.15.1)
Requirement already satisfied: safetensors>=0.3.1 in /usr/local/lib/python3.10/dist-packages (from transformers[torch]) (0.4.2)
Requirement already satisfied: tqdm>=4.27 in /usr/local/lib/python3.10/dist-packages (from transformers[torch]) (4.66.1)
Requirement already satisfied: torch!=1.12.0,>=1.10 in /usr/local/lib/python3.10/dist-packages (from transformers[torch]) (2.1.0+cu121)
Collecting accelerate>=0.20.3 (from transformers[torch])
  Downloading accelerate-0.26.1-py3-none-any.whl (270 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 270.9/270.9 kB 7.4 MB/s eta 0:00:00
Requirement already satisfied: psutil in /usr/local/lib/python3.10/dist-packages (from accelerate>=0.20.3->transformers[torch]) (5.9.5)
Requirement already satisfied: fsspec>=2023.5.0 in /usr/local/lib/python3.10/dist-packages (from huggingface-hub<1.0,>=0.16.4->transformers[torch]) (2023.6.0)
Requirement already satisfied: typing-extensions>=3.7.4.3 in /usr/local/lib/python3.10/dist-packages (from huggingface-hub<1.0,>=0.16.4->transformers[torch]) (4.5.0)
Requirement already satisfied: sympy in /usr/local/lib/python3.10/dist-packages (from torch!=1.12.0,>=1.10->transformers[torch]) (1.12)
Requirement already satisfied: networkx in /usr/local/lib/python3.10/dist-packages (from torch!=1.12.0,>=1.10->transformers[torch]) (3.2.1)
Requirement already satisfied: jinja2 in /usr/local/lib/python3.10/dist-packages (from torch!=1.12.0,>=1.10->transformers[torch]) (3.1.3)
Requirement already satisfied: triton==2.1.0 in /usr/local/lib/python3.10/dist-packages (from torch!=1.12.0,>=1.10->transformers[torch]) (2.1.0)
Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests->transformers[torch]) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/dist-packages (from requests->transformers[torch]) (3.6)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests->transformers[torch]) (2.0.7)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/dist-packages (from requests->transformers[torch]) (2023.11.17)
Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.10/dist-packages (from jinja2->torch!=1.12.0,>=1.10->transformers[torch]) (2.1.4)
Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.10/dist-packages (from sympy->torch!=1.12.0,>=1.10->transformers[torch]) (1.3.0)
Installing collected packages: accelerate
Successfully installed accelerate-0.26.1

kcELECTRA Fine tuning

kcELECTRA 설명

  • korean comment(한국어 댓글)을 데이터셋으로 사용해 다소 noisy한 user-generated text에서도 잘 작동하도록 사전학습한 모델임.
  • 뉴스 기사들에서 댓글과 대댓글을 수집하여 1억 8천만개 이상의 문장으로 Pre-train한 모델

https://github.com/Beomi/KcELECTRA

Fine tuning process

요약

  1. 시드 고정
  2. 데이터 로드
  3. 데이터 스플릿
  4. Trainer 불러와서 학습
  5. 평가

시드 고정

  • 모델의 재현성(reproductiblity)을 위해 시드를 고정한다.
In [ ]:
# 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
In [ ]:
!git clone https://github.com/bab2min/corpus.git
Cloning into 'corpus'...
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 15 (delta 2), reused 11 (delta 1), pack-reused 0
Receiving objects: 100% (15/15), 11.78 MiB | 15.75 MiB/s, done.
Resolving deltas: 100% (2/2), done.
In [ ]:
import pandas as pd

# 네이버 쇼핑 리뷰
naver_shopping = pd.read_csv('corpus/sentiment/naver_shopping.txt', sep='\t', header=None, names=['sentiment', 'text'])
naver_shopping
Out[ ]:
sentiment text
0 5 배공빠르고 굿
1 2 택배가 엉망이네용 저희집 밑에층에 말도없이 놔두고가고
2 5 아주좋아요 바지 정말 좋아서2개 더 구매했어요 이가격에 대박입니다. 바느질이 조금 ...
3 2 선물용으로 빨리 받아서 전달했어야 하는 상품이었는데 머그컵만 와서 당황했습니다. 전...
4 5 민트색상 예뻐요. 옆 손잡이는 거는 용도로도 사용되네요 ㅎㅎ
... ... ...
199995 2 장마라그런가!!! 달지않아요
199996 5 다이슨 케이스 구매했어요 다이슨 슈퍼소닉 드라이기 케이스 구매했어요가격 괜찮고 배송...
199997 5 로드샾에서 사는것보다 세배 저렴하네요 ㅜㅜ 자주이용할께요
199998 5 넘이쁘고 쎄련되보이네요~
199999 5 아직 사용해보지도않았고 다른 제품을 써본적이없어서 잘 모르겠지만 ㅎㅎ 배송은 빨랐습니다

200000 rows × 2 columns

In [ ]:
naver_shopping['sentiment'] = naver_shopping['sentiment'].apply(lambda x: 1 if x > 3 else 0)
In [ ]:
naver_shopping
Out[ ]:
sentiment text
0 1 배공빠르고 굿
1 0 택배가 엉망이네용 저희집 밑에층에 말도없이 놔두고가고
2 1 아주좋아요 바지 정말 좋아서2개 더 구매했어요 이가격에 대박입니다. 바느질이 조금 ...
3 0 선물용으로 빨리 받아서 전달했어야 하는 상품이었는데 머그컵만 와서 당황했습니다. 전...
4 1 민트색상 예뻐요. 옆 손잡이는 거는 용도로도 사용되네요 ㅎㅎ
... ... ...
199995 0 장마라그런가!!! 달지않아요
199996 1 다이슨 케이스 구매했어요 다이슨 슈퍼소닉 드라이기 케이스 구매했어요가격 괜찮고 배송...
199997 1 로드샾에서 사는것보다 세배 저렴하네요 ㅜㅜ 자주이용할께요
199998 1 넘이쁘고 쎄련되보이네요~
199999 1 아직 사용해보지도않았고 다른 제품을 써본적이없어서 잘 모르겠지만 ㅎㅎ 배송은 빨랐습니다

200000 rows × 2 columns

In [ ]:
steam = pd.read_csv('corpus/sentiment/steam.txt', sep='\t', header=None, names=['sentiment', 'text'])
steam
Out[ ]:
sentiment text
0 0 노래가 너무 적음
1 0 돌겠네 진짜. 황숙아, 어크 공장 그만 돌려라. 죽는다.
2 1 막노동 체험판 막노동 하는사람인데 장비를 내가 사야돼 뭐지
3 1 차악!차악!!차악!!! 정말 이래서 왕국을 되찾을 수 있는거야??
4 1 시간 때우기에 좋음.. 도전과제는 50시간이면 다 깰 수 있어요
... ... ...
99995 0 한글화해주면 10개산다
99996 0 개쌉노잼 ㅋㅋ
99997 0 노잼이네요... 30분하고 지웠어요...
99998 1 야생을 사랑하는 사람들을 위한 짧지만 여운이 남는 이야기. 영어는 그리 어렵지 않습니다.
99999 1 한국의 메탈레이지를 떠오르게한다 진짜 손맛으로 하는게임

100000 rows × 2 columns

In [ ]:
data = pd.concat(naver_shopping, steam)

데이터 스플릿

  • 학습 때 사용할 train, val 데이터와,
  • 테스트할 test 데이터로 나눔

Data split

  • 학습을 위한 train set, 최종 평가를 위한 test set, hyper parameter tuning을 위한 validation set으로 스플릿
  • train data에서 5%만 떼서 validation용으로 사용
In [ ]:
# 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)
In [ ]:
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)
In [ ]:
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]은 각각 문장의 시작과 끝을 나타낸다.
In [ ]:
from transformers import AutoTokenizer

model_name = "beomi/KcELECTRA-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)
/usr/local/lib/python3.10/dist-packages/huggingface_hub/utils/_token.py:88: UserWarning: 
The secret `HF_TOKEN` does not exist in your Colab secrets.
To authenticate with the Hugging Face Hub, create a token in your settings tab (https://huggingface.co/settings/tokens), set it as secret in your Google Colab and restart your session.
You will be able to reuse this secret in all of your notebooks.
Please note that authentication is recommended but still optional to access public models or datasets.
  warnings.warn(
In [ ]:
# 모델의 인풋값은 텍스트가 아닌 tokenizer를 통해 인코딩된 값이 들어가므로, 인코딩을 미리 진행함
train_encoding = tokenizer(x_train.values.tolist(), truncation=True, padding=True)
val_encoding = tokenizer(x_val.values.tolist(), truncation=True, padding=True)
In [ ]:
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)
In [ ]:
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에서활용)
In [ ]:
# 디버깅용
import os
os.environ['CUDA_LAUNCH_BLOCKING']= '1'
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
In [ ]:
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()
Some weights of ElectraForSequenceClassification were not initialized from the model checkpoint at beomi/KcELECTRA-base and are newly initialized: ['classifier.dense.bias', 'classifier.out_proj.bias', 'classifier.dense.weight', 'classifier.out_proj.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
[ 961/4952 1:26:23 < 5:59:31, 0.19 it/s, Epoch 0.39/2]
Step Training Loss Validation Loss
10 0.694500 0.693701
20 0.694400 0.693623
30 0.695300 0.693490
40 0.693700 0.693298
50 0.693500 0.693071
60 0.692300 0.692759
70 0.692100 0.692406
80 0.694400 0.692039
90 0.692800 0.691562
100 0.690900 0.691055
110 0.692500 0.690480
120 0.687800 0.689740
130 0.690300 0.688799
140 0.690100 0.687467
150 0.684700 0.685400
160 0.686400 0.682419
170 0.685100 0.677926
180 0.682200 0.670044
190 0.670200 0.657169
200 0.656900 0.635395
210 0.628100 0.606271
220 0.597000 0.573772
230 0.571100 0.541201
240 0.545800 0.514000
250 0.514100 0.489983
260 0.498500 0.469369
270 0.484300 0.454861
280 0.463400 0.441461
290 0.455800 0.427361
300 0.433400 0.417152
310 0.439400 0.409130
320 0.416400 0.400570
330 0.425100 0.394168
340 0.421200 0.388143
350 0.416400 0.385212
360 0.404300 0.381197
370 0.388700 0.376628
380 0.405800 0.375147
390 0.396500 0.367583
400 0.386400 0.363242
410 0.379000 0.359976
420 0.377400 0.358616
430 0.370500 0.353233
440 0.396200 0.350896
450 0.373600 0.347506
460 0.363300 0.342865
470 0.384400 0.344124
480 0.348100 0.339847
490 0.356600 0.338150
500 0.354600 0.335601
510 0.326600 0.333860
520 0.345700 0.330594
530 0.346800 0.328173
540 0.321800 0.325051
550 0.342500 0.330093
560 0.352700 0.320783
570 0.358400 0.319338
580 0.370700 0.319454
590 0.338800 0.313736
600 0.332100 0.311550
610 0.331200 0.311709
620 0.314000 0.309340
630 0.326500 0.308856
640 0.333900 0.309668
650 0.329800 0.304582
660 0.313300 0.306496
670 0.338700 0.305183
680 0.306700 0.302734
690 0.314700 0.303834
700 0.341200 0.300882
710 0.330400 0.300436
720 0.295100 0.299764
730 0.312100 0.298454
740 0.320800 0.296710
750 0.300700 0.295356
760 0.309500 0.297959
770 0.334100 0.296122
780 0.307200 0.292616
790 0.336400 0.293803
800 0.292800 0.294398
810 0.299900 0.297184
820 0.320400 0.290907
830 0.325300 0.289678
840 0.300500 0.288184
850 0.320000 0.290339
860 0.294300 0.293132
870 0.289000 0.287910
880 0.292500 0.287394
890 0.299100 0.285552
900 0.269600 0.286179
910 0.313700 0.285448
920 0.275500 0.288386
930 0.317100 0.285855
940 0.312800 0.283966
950 0.287500 0.284039

[174/522 00:14 < 00:28, 12.02 it/s]
In [ ]:
# 모델 저장

trainer.save_model("./result/20240206/models")
In [ ]:
 

Evaluate

  • 모델의 아웃풋 값은 logit이다.
  • 즉 이는 마지막 노드의 출력값이고, output layer에 들어가기 전 값이다.
  • sigmoid에 넣어주어 최종 아웃풋 값을 반환받는다
In [1]:
# 토크나이저 불러오기
from transformers import AutoTokenizer

model_name = "beomi/KcELECTRA-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)
/usr/local/lib/python3.10/dist-packages/huggingface_hub/utils/_token.py:88: UserWarning: 
The secret `HF_TOKEN` does not exist in your Colab secrets.
To authenticate with the Hugging Face Hub, create a token in your settings tab (https://huggingface.co/settings/tokens), set it as secret in your Google Colab and restart your session.
You will be able to reuse this secret in all of your notebooks.
Please note that authentication is recommended but still optional to access public models or datasets.
  warnings.warn(
In [6]:
# fine tuning한 모델 불러오기
from transformers import AutoModel, AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained('./result/20240206/models')
In [7]:
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
In [ ]:
import pandas as pd

# 위에서 스플릿한 테스트 데이터 외에, 실제 추론에 사용할 데이터를 가져와서 테스트하였다.(학습때 노출 x)
test = pd.read_csv('./result/test_result.csv', index_col=0)
test[['text', 'sentiment']]
In [11]:
pred = test['text'].apply(predict)
In [18]:
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')
Out[18]:
<sklearn.metrics._plot.confusion_matrix.ConfusionMatrixDisplay at 0x796652f05600>
In [ ]:
In [ ]: