오답노트

[NLP] koBERT 실습 본문

Python/DL

[NLP] koBERT 실습

권멋져 2022. 10. 15. 00:43

koBERT

https://github.com/SKTBrain/KoBERT

 

GitHub - SKTBrain/KoBERT: Korean BERT pre-trained cased (KoBERT)

Korean BERT pre-trained cased (KoBERT). Contribute to SKTBrain/KoBERT development by creating an account on GitHub.

github.com

기존 BERT는 140개의 언어셋으로 학습된 모델이다.

하지만 한국어에 대한 성능 한계를 극복하고자 한국어 위키백과를 학습시킨 모델이 바로 koBERT이다.

 

본 포스트는 koBERT를 불러와 사용하는 방법을 요약했다.

코드의 원본은 아래와 같다.

 

코드 원본

 

naver_review_classifications_pytorch_kobert.ipynb

Run, share, and edit Python notebooks

colab.research.google.com

 

코드

원본 코드는 감성분류로 해당 문장의 긍정 또는 부정을 판별하는 코드이다.

나는 감성분류는 아니지만 문장을 binary classification으로 나눠야 하는 공통점을 가지고 있다.

이 점을 참고하여 소스를 참고해주길 바란다.

 

koBERT Download & Install

!pip install git+https://git@github.com/SKTBrain/KoBERT.git@master
!git clone https://github.com/SKTBrain/KoBERT.git
!pip install -r /content/KoBERT/requirements.txt

위 명령어를 통해 git에서 koBERT 설치에 필요한 파일들을 다운로드 받고, requirements.txt로 koBERT를 불러올 수 있도록 하자

 

패키지 및 라이브러리 호출

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
import pandas as pd
from tqdm.notebook import tqdm

from kobert import get_tokenizer
from kobert import get_pytorch_kobert_model

from transformers import AdamW
from transformers.optimization import get_cosine_schedule_with_warmup

pytorch를 사용하기 때문에 pytorch 관련 라이브러리를 호출하고, 이전에 설치한 koBERT에 호출에 필요한 패키지도 불러온다.

 

 

데이터 및 모델 불러오기

dataset_train = nlp.data.TSVDataset("/content/train.tsv", field_indices=[0,1], num_discard_samples=1,allow_missing=True)
dataset_test = nlp.data.TSVDataset("/content/test.tsv", field_indices=[1,2], num_discard_samples=1,allow_missing=True)

bertmodel, vocab = get_pytorch_kobert_model(cachedir=".cache")

tokenizer = get_tokenizer()
tok = nlp.data.BERTSPTokenizer(tokenizer, vocab, lower=False)

train 데이터와 test 데이터 모두 tsv 파일로 읽어서 가져왔다.

tsv파일은 구분자가 탭(\t)로 되어 있는 양식을 가진 파일이다.

 

tsv파일을 가져올때 사용한 TSVDataset 함수는

가져올 영역을 field_indixes 옵션을 통해서 정할 수 있으며,

num_discard_samples를 통해 데이터 상단으로 부터 제회할 row의 개수를 설정할 수 있다.

allow_missing은 필드 수가 제공된 최대 필드 인덱스보다 작은 경우에 예외처리를 하지 않는다.

 

CustomDataset 정의

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([data[sent_idx]]) for data in dataset]
        self.labels = [np.int32(data[label_idx]) for data in dataset]

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

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

 

BERTSentenceTransform은 Input token id array, vaild length, Input token type을 반환한다.

그러므로 BERTDataset에 인덱스로 원소를 호출 할때에는 위 3개의 데이터와 label을 반환한다.

 

Config 변수 선언

max_len = 84 #문자열 길이
batch_size = 64
warmup_ratio = 0.1
num_epochs = 5
max_grad_norm = 1
log_interval = 10
learning_rate =  5e-5


device = torch.device('cuda:0') if torch.cuda.is_available() else torch.device('cpu')

 

Dataset 와 Dataloader 선언

data_train = BERTDataset(dataset=dataset_train, sent_idx=0, label_idx=1, 
                         bert_tokenizer=tok, max_len=max_len, pad=True, pair=False)

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

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

 

BERTDataset

  • dataset : tsv로부터 불러온 변수를 입력
  • sent_idx : dataset에서 문장의 인덱스
  • label_idx : dataset에서 라벨의 인덱스
  • bert_tokenizer : 토크나이저를 입력
  • max_len : 문장의 최대길이를 입력한다
  • pad : True일 때, 문장 최대길이보다 짧은 길이의 문장들은 문장의 최대 길이 만큼 0으로 채운다
  • pair : 문장 2개를 비교하여 길이가 짧은 문장 크기에 긴 문장을 줄여서 맞춘다.

 

DataLoader

  • dataset : Dataset 변수를 입력
  • batch_size : batch size 를 입력한다
  • num_workers : 멀티 프로세싱을 통해 설정한 값 만큼 worker를 생성해 동시에 Data를 Load한다.

 

 

모델 정의

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), return_dict=False)   # <-- return_dict=False 추가했습니다
        if self.dr_rate:
            out = self.dropout(pooler)
        return self.classifier(out)
        
  model = BERTClassifier(bertmodel,  dr_rate=0.5).to(device)

위 코드는 원본 코랩 코드와 다르다. model을 evaluation할 때, 오류가 발생해서 위 코드로 바꾸게 되었다.

 

num_classes 변수를 조절하여 binary classification 뿐만 아니라 multi classification 문제도 해결할 수 있다.

 

학습에 필요한 옵션 선언

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

 

optimizer_grouped_parameters 는 no_decay가 포함된 파라미터와 포함되지 않은 파라미터를 나눠서 저장한다.

 

학습

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 tqdm(enumerate(train_dataloader), total=len(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 tqdm(enumerate(test_dataloader), total=len(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)))

 

최종 학습 결과는 다음과 같다.

acc 0.9543425324675324

'Python > DL' 카테고리의 다른 글

[밑딥] 신경망  (0) 2023.01.21
[밑딥] 퍼셉트론  (1) 2023.01.21
[NLP] N-gram  (0) 2022.10.14
[NLP] BERT  (0) 2022.10.07
[NLP] Transformer  (0) 2022.10.07