관리 메뉴

개발자의 스터디 노트

KoBERT로 이진 분류 학습 본문

파이썬/파이토치 자연어처리

KoBERT로 이진 분류 학습

박개발씨 2022. 3. 12. 01:37

우선 학습하고자 하는 데이터를 준비합니다.

여기서는 리포트의 외향 내향 분석을 하기 위해 따로 자료를 준비해두었습니다.

2진 분류 데이터라면 어떠한 데이터도 상관없으니 준비하여 학습하면 됩니다.

 

1. 학습에 필요한 패키지 import

import pandas as pd
import csv

## Import
import torch
from torch import nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import gluonnlp as nlp
import numpy as np
from tqdm import tqdm, tqdm_notebook

#KoBERT
from kobert.utils import get_tokenizer
from kobert.pytorch_kobert import get_pytorch_kobert_model

#transformers
from transformers import AdamW
from transformers.optimization import get_cosine_schedule_with_warmup

 

2. GPU 설정

#GPU 사용
device = torch.device("cuda:0")

 

3. KoBERT모델, Vocabulary 불러오기

#BERT 모델, Vocabulary 불러오기
bertmodel, vocab = get_pytorch_kobert_model()
/home/JupyterNotebook/pytorch_ex/.cache/kobert_v1.zip[██████████████████████████████████████████████████]
/home/JupyterNotebook/pytorch_ex/.cache/kobert_news_wiki_ko_cased-1087f8699e.spiece[██████████████████████████████████████████████████]

 

4. 데이터 로딩

# csv data 읽어서 학습에 사용할 데이터로 변경
def load_csv(csv_path):
    f = open(csv_path, 'r', encoding='utf-8')
    lines = csv.reader(f)    
    data_list = []
    for rpt_parent,propensity in lines:
        data = []
        data.append(rpt_parent)
        data.append(str(propensity))
        data_list.append(data)
    f.close()
    return data_list



## 데이터 준비
report_path = "data/report.csv"
data_list = load_csv(report_path)
print(data_list[0:2])
[
['정석대로라면 월간계획표를 진행해야하는 시기이나, ..... 확인되었습니다. 감사합니다.', '1'], 
['지난 시간에 내주었던 숙제 1장 반 중 절반정도의 ..... 넘어가도록 하겠습니다.', '0']
]

출력된것을 확인하면 ['sentence', 'class']의 형태로 이루어져 있는 것을 확인할 수 있고, class는 숫자로 라벨링 되어있는 것을 확인할 수 있습니다.

 

5. 학습 데이터와 테스트 데이터로 나누기

#train & test 데이터로 나누기
from sklearn.model_selection import train_test_split
                                                         
dataset_train, dataset_test = train_test_split(data_list, test_size=0.25, random_state=0)

print(len(dataset_train))
print(len(dataset_test))
3907
1303

테스트 데이터가 25% 나뉘어 있는것을 확인할 수 있습니다.

 

6. KoBERT 입력 데이터로 만들기

토큰화, 정수 인코딩, 패딩 들을 해주는 클래스입니다.

class BERTDataset(Dataset):
    def __init__(self, dataset, sent_idx, label_idx, bert_tokenizer, max_len,
                 pad, pair):
        transform = nlp.data.BERTSentenceTransform(
            bert_tokenizer, max_seq_length=max_len, pad=pad, pair=pair)

        self.sentences = [transform([i[sent_idx]]) for i in dataset]
        self.labels = [np.int32(i[label_idx]) for i in dataset]

    def __getitem__(self, i):
        return (self.sentences[i] + (self.labels[i], ))

    def __len__(self):
        return (len(self.labels))

 

7. 하이퍼 파라미터 설정

# Setting parameters
max_len = 64
batch_size = 64
warmup_ratio = 0.1
num_epochs = 5
max_grad_norm = 1
log_interval = 200
learning_rate =  5e-5

KoBERT 학습을 위한 하이퍼 파라메터 정보입니다.

max_len : 학습할때 사용되는 단어의 최대 길이 제한 입니다.
자연어 기반 데이터 학습을위해 단어 사이즈 제한을 둡니다.
 
batch_size : 몇개의 샘플로 가중치를 갱신할 것인지 설정합니다.
 
warmup_ratio : 워밍업 수치
워밍업은 초기 과적합 문제를 예방하는 기법으로 많은 학습 모델에서
파라메터 옵션으로 제공합니다.
 
num_epochs : 전체 데이터 셋을 몇번 반복학습할지 설정합니다.
 
max_grad_norm : 
러닝 학습에서 기울기 gradient의 크기
norm의 크기를 제한함으로써 기울기 벡터(gradient vector)의 방향은 유지하되,
그 크기는 학습이 망가지지 않을 정도로 줄어들 수 있습니다.
 
log_interval : 로그 간격
 
learning_rate : 학습비율
적절한 수치를 찾는 공식은 따로 없으며, 수치에 따라 경사면을 따라 내려가서
최적의 값을 찾고 종료되어야 하나 learning_rate가 너무 클경우 경사면을 타고 
올라가는 overshooting이 발생할 수 있습니다.

 

8. 토큰화와 패딩 실행

#토큰화
tokenizer = get_tokenizer()
tok = nlp.data.BERTSPTokenizer(tokenizer, vocab, lower=False)

data_train = BERTDataset(dataset_train, 0, 1, tok, max_len, True, False)
data_test = BERTDataset(dataset_test, 0, 1, tok, max_len, True, False)

토큰화와 패딩이 잘 되었는지를 확인하기 위해 한건만 출력해 봅시다.

data_train[0]
(array([   2, 2890, 6706, 1436, 4389, 7088, 2086, 7798, 5330, 1919, 6896,
        2408, 6234, 2120, 7178, 2912, 6553, 6312, 6940, 4930, 2125, 4888,
        6199,  834, 3084, 5760, 2963,  517, 5330, 7248, 5761, 1741, 7903,
        7096, 5330, 3647, 2437, 6903, 4389, 6064, 2387, 6199, 3367, 4930,
        2058, 2355, 6948,  517,   54, 2415, 3836, 7078, 5859, 3084, 5760,
        5400, 3978, 5782, 5439,  517,   46, 2890, 6706,    3], dtype=int32),
 array(64, dtype=int32),
 array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       dtype=int32),
 1)

출력 값들을 보면 3개의 array가 출력됩니다.

첫 번째는 패딩 된 시퀀스

두 번째는 길이와 타입에 대한 내용

세 번째는 어텐션 마스크 시퀀스입니다.

BERT에 데이터가 입력되었을 때 어텐션 함수가 적용되어 연산이 됩니다. 이때 1로 패딩 된 값들을 연산할 필요가 없기 때문에 연산을 하지 ㅇ낳아도 된다고 알려주는 데이터가 있어야 하는 게 그게 바로 어텐션 마스크 시퀀스인 것입니다. BERT나 KoBERT에는 어텐션 마스크 데이터도 함께 입력되어야 합니다.

 

9. torch의 dataset 만들기

train_dataloader = torch.utils.data.DataLoader(data_train, batch_size=batch_size, num_workers=5)
test_dataloader = torch.utils.data.DataLoader(data_test, batch_size=batch_size, num_workers=5)

 

10. BERT 분류기

num_classes=2 인 부분을 수정하여 다항 분류 학습에 이용할 수 있습니다.

class BERTClassifier(nn.Module):
    def __init__(self,
                 bert,
                 hidden_size = 768,
                 num_classes=2,   ##클래스 수 조정##
                 dr_rate=None,
                 params=None):
        super(BERTClassifier, self).__init__()
        self.bert = bert
        self.dr_rate = dr_rate
                 
        self.classifier = nn.Linear(hidden_size , num_classes)
        if dr_rate:
            self.dropout = nn.Dropout(p=dr_rate)
    
    def gen_attention_mask(self, token_ids, valid_length):
        attention_mask = torch.zeros_like(token_ids)
        for i, v in enumerate(valid_length):
            attention_mask[i][:v] = 1
        return attention_mask.float()

    def forward(self, token_ids, valid_length, segment_ids):
        attention_mask = self.gen_attention_mask(token_ids, valid_length)
        
        _, pooler = self.bert(input_ids = token_ids, token_type_ids = segment_ids.long(), attention_mask = attention_mask.float().to(token_ids.device))
        if self.dr_rate:
            out = self.dropout(pooler)
        return self.classifier(out)

 

11. 학습하기

#BERT 모델 불러오기
model = BERTClassifier(bertmodel,  dr_rate=0.5).to(device)

#optimizer와 schedule 설정
no_decay = ['bias', 'LayerNorm.weight']
optimizer_grouped_parameters = [
    {'params': [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)], 'weight_decay': 0.01},
    {'params': [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
]

optimizer = AdamW(optimizer_grouped_parameters, lr=learning_rate)
loss_fn = nn.CrossEntropyLoss()

t_total = len(train_dataloader) * num_epochs
warmup_step = int(t_total * warmup_ratio)

scheduler = get_cosine_schedule_with_warmup(optimizer, num_warmup_steps=warmup_step, num_training_steps=t_total)

#정확도 측정을 위한 함수 정의
def calc_accuracy(X,Y):
    max_vals, max_indices = torch.max(X, 1)
    train_acc = (max_indices == Y).sum().data.cpu().numpy()/max_indices.size()[0]
    return train_acc
    
train_dataloader
for e in range(num_epochs):
    train_acc = 0.0
    test_acc = 0.0
    model.train()
    for batch_id, (token_ids, valid_length, segment_ids, label) in enumerate(tqdm(train_dataloader)):
        optimizer.zero_grad()
        token_ids = token_ids.long().to(device)
        segment_ids = segment_ids.long().to(device)
        valid_length= valid_length
        label = label.long().to(device)
        out = model(token_ids, valid_length, segment_ids)
        loss = loss_fn(out, label)
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_grad_norm)
        optimizer.step()
        scheduler.step()  # Update learning rate schedule
        train_acc += calc_accuracy(out, label)
        if batch_id % log_interval == 0:
            print("epoch {} batch id {} loss {} train acc {}".format(e+1, batch_id+1, loss.data.cpu().numpy(), train_acc / (batch_id+1)))
    print("epoch {} train acc {}".format(e+1, train_acc / (batch_id+1)))
    
    model.eval()
    for batch_id, (token_ids, valid_length, segment_ids, label) in enumerate(tqdm(test_dataloader)):
        token_ids = token_ids.long().to(device)
        segment_ids = segment_ids.long().to(device)
        valid_length= valid_length
        label = label.long().to(device)
        out = model(token_ids, valid_length, segment_ids)
        test_acc += calc_accuracy(out, label)
    print("epoch {} test acc {}".format(e+1, test_acc / (batch_id+1)))
 3%|▎         | 2/62 [00:00<00:20,  2.90it/s]
  5%|▍         | 3/62 [00:00<00:18,  3.27it/s]
  6%|▋         | 4/62 [00:01<00:16,  3.49it/s]
  8%|▊         | 5/62 [00:01<00:15,  3.62it/s]
 10%|▉         | 6/62 [00:01<00:15,  3.72it/s]
 11%|█▏        | 7/62 [00:02<00:14,  3.79it/s]
 13%|█▎        | 8/62 [00:02<00:14,  3.83it/s]
 15%|█▍        | 9/62 [00:02<00:13,  3.86it/s]
 16%|█▌        | 10/62 [00:02<00:13,  3.88it/s]
 18%|█▊        | 11/62 [00:03<00:13,  3.89it/s]
 19%|█▉        | 12/62 [00:03<00:12,  3.90it/s]
 21%|██        | 13/62 [00:03<00:12,  3.90it/s]
 23%|██▎       | 14/62 [00:03<00:12,  3.90it/s]
 24%|██▍       | 15/62 [00:04<00:12,  3.90it/s]
 26%|██▌       | 16/62 [00:04<00:11,  3.90it/s]
 27%|██▋       | 17/62 [00:04<00:11,  3.90it/s]
 29%|██▉       | 18/62 [00:04<00:11,  3.90it/s]
 31%|███       | 19/62 [00:05<00:11,  3.90it/s]
 32%|███▏      | 20/62 [00:05<00:10,  3.91it/s]
 34%|███▍      | 21/62 [00:05<00:10,  3.90it/s]
 35%|███▌      | 22/62 [00:05<00:10,  3.90it/s]
 37%|███▋      | 23/62 [00:06<00:09,  3.90it/s]
 39%|███▊      | 24/62 [00:06<00:09,  3.90it/s]
 40%|████      | 25/62 [00:06<00:09,  3.91it/s]
 42%|████▏     | 26/62 [00:06<00:09,  3.91it/s]
 44%|████▎     | 27/62 [00:07<00:08,  3.91it/s]
 45%|████▌     | 28/62 [00:07<00:08,  3.91it/s]
 47%|████▋     | 29/62 [00:07<00:08,  3.91it/s]
 48%|████▊     | 30/62 [00:07<00:08,  3.91it/s]
 50%|█████     | 31/62 [00:08<00:07,  3.91it/s]
 52%|█████▏    | 32/62 [00:08<00:07,  3.90it/s]
 53%|█████▎    | 33/62 [00:08<00:07,  3.90it/s]
 55%|█████▍    | 34/62 [00:08<00:07,  3.90it/s]
 56%|█████▋    | 35/62 [00:09<00:06,  3.88it/s]
 58%|█████▊    | 36/62 [00:09<00:06,  3.88it/s]
 60%|█████▉    | 37/62 [00:09<00:06,  3.89it/s]
 61%|██████▏   | 38/62 [00:09<00:06,  3.89it/s]
 63%|██████▎   | 39/62 [00:10<00:05,  3.89it/s]
 65%|██████▍   | 40/62 [00:10<00:05,  3.90it/s]
 66%|██████▌   | 41/62 [00:10<00:05,  3.91it/s]
 68%|██████▊   | 42/62 [00:10<00:05,  3.91it/s]
 69%|██████▉   | 43/62 [00:11<00:04,  3.91it/s]
 71%|███████   | 44/62 [00:11<00:04,  3.92it/s]
 73%|███████▎  | 45/62 [00:11<00:04,  3.92it/s]
 74%|███████▍  | 46/62 [00:11<00:04,  3.92it/s]
 76%|███████▌  | 47/62 [00:12<00:03,  3.92it/s]
 77%|███████▋  | 48/62 [00:12<00:03,  3.91it/s]
 79%|███████▉  | 49/62 [00:12<00:03,  3.91it/s]
 81%|████████  | 50/62 [00:13<00:03,  3.91it/s]
 82%|████████▏ | 51/62 [00:13<00:02,  3.91it/s]
 84%|████████▍ | 52/62 [00:13<00:02,  3.91it/s]
 85%|████████▌ | 53/62 [00:13<00:02,  3.91it/s]
 87%|████████▋ | 54/62 [00:14<00:02,  3.91it/s]
 89%|████████▊ | 55/62 [00:14<00:01,  3.91it/s]
 90%|█████████ | 56/62 [00:14<00:01,  3.91it/s]
 92%|█████████▏| 57/62 [00:14<00:01,  3.91it/s]
 94%|█████████▎| 58/62 [00:15<00:01,  3.91it/s]
 95%|█████████▌| 59/62 [00:15<00:00,  3.91it/s]
 97%|█████████▋| 60/62 [00:15<00:00,  3.91it/s]
 98%|█████████▊| 61/62 [00:15<00:00,  3.91it/s]
100%|██████████| 62/62 [00:16<00:00,  3.85it/s]
epoch 5 train acc 0.8500504032258065
  0%|          | 0/21 [00:00<?, ?it/s]
  5%|▍         | 1/21 [00:00<00:05,  3.61it/s]
 10%|▉         | 2/21 [00:00<00:03,  5.77it/s]
 19%|█▉        | 4/21 [00:00<00:01,  8.63it/s]
 29%|██▊       | 6/21 [00:00<00:01,  9.90it/s]
 38%|███▊      | 8/21 [00:00<00:01, 10.69it/s]
 48%|████▊     | 10/21 [00:01<00:00, 11.16it/s]
 57%|█████▋    | 12/21 [00:01<00:00, 11.46it/s]
 67%|██████▋   | 14/21 [00:01<00:00, 11.64it/s]
 76%|███████▌  | 16/21 [00:01<00:00, 11.75it/s]
 86%|████████▌ | 18/21 [00:01<00:00, 11.82it/s]
100%|██████████| 21/21 [00:01<00:00, 10.80it/s]
epoch 5 test acc 0.7050012939958592

훈련 정확도는 85% 테스트 정확도는 70% 수준을 가지고 있습니다.

 

12. 새로운 데이터로 테스트 하기

#토큰화
tokenizer = get_tokenizer()
tok = nlp.data.BERTSPTokenizer(tokenizer, vocab, lower=False)

def predict(predict_sentence):

    data = [predict_sentence, '0']
    dataset_another = [data]

    another_test = BERTDataset(dataset_another, 0, 1, tok, max_len, True, False)
    test_dataloader = torch.utils.data.DataLoader(another_test, batch_size=batch_size, num_workers=5)
    
    model.eval()

    for batch_id, (token_ids, valid_length, segment_ids, label) in enumerate(test_dataloader):
        token_ids = token_ids.long().to(device)
        segment_ids = segment_ids.long().to(device)

        valid_length= valid_length
        label = label.long().to(device)

        out = model(token_ids, valid_length, segment_ids)


        test_eval=[]
        for i in out:
            logits=i
            logits = logits.detach().cpu().numpy()

            if np.argmax(logits) == 0:
                test_eval.append("내향")
            elif np.argmax(logits) == 1:
                test_eval.append("외향")
            
        print(">> 입력하신 레포트는 " + test_eval[0])
#질문 무한반복하기! 0 입력시 종료
end = 1
while end == 1 :
    sentence = input("레포트 내용을 입력해주세요 : ")
    if sentence == 0 :
        break
    predict(sentence)
    print("\n")

새로운 리포트를 입력하여 결과를 테스트해볼 수 있습니다.