오답노트
[NLP] koBERT 실습 본문
koBERT
https://github.com/SKTBrain/KoBERT
기존 BERT는 140개의 언어셋으로 학습된 모델이다.
하지만 한국어에 대한 성능 한계를 극복하고자 한국어 위키백과를 학습시킨 모델이 바로 koBERT이다.
본 포스트는 koBERT를 불러와 사용하는 방법을 요약했다.
코드의 원본은 아래와 같다.
코드
원본 코드는 감성분류로 해당 문장의 긍정 또는 부정을 판별하는 코드이다.
나는 감성분류는 아니지만 문장을 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 |