BERT와 KoBERT(Word-Piece Embedding, 코드리뷰)
BERT : Pre-training of Deep Bidirectional Trnasformers for Language Understanding
BERT 모델은 Transformer의 Encoder 모델을 겹겹이 쌓아 만든 모델이다. BERT의 경우 Encoder가 매우 많기 때문에 복잡한 모델에 속한다. 때문에 일반적인 기업이나 개인이 학습하기 어렵다.
이런 이유들로 인해 BERT는 대형 기업들이 학습을 해놓은 모델을 가져다가 fine-tuning 방법으로 사용하는 것이 맞다.
위와 같이 기존에 pre-trained 된 BERT 모델을 fine-tuning하여 다양한 task에 적용시키는 것을 추천한다고 논문에서는 언급하고 있다.
BERT는 다른 자연어처리 모델들이 적용하는 word embedding기법과는 다르게 WordPiece embedding기법을 사용한다.
Word Piece Embedding
일반적인 word embedding은 이전에 Word2Vec이나 TF-IDF, CBOW, Skip-gram을 활용하는 기법으로 word를 vectorize한다. 하지만 word embedding을 통한 vectorize의 경우 제한적인 vocab을 가지고 있을 때 word를 벡터화하는데에 한계가 있다. 사전에 없는 단어의 경우에는 벡터화 자체를 진행하지 않아 소중한 정보가 손실될 수 있다는 것이다.
또한 word embedding 기법의 경우에는 embedding vector로 만들 수 있도록 CBOW나 skip-gram과 같은 기법에 의해 학습되어야한다. 때문에 단어가 많아질수록 학습에 필요한 비용이 높아지게 되는 단점이 있다.
BERT에서는 Word-Piece Embedding은 이런 word embedding의 한계를 극복하고 제한적인 vocab으로도 데이터를 표현할 수 있게 한다. 즉, OOV(Out-Of-Vocabulary)를 해결한다.
Word-Piece의 원리는 다음과 같다.
- 문장에 등장하는 단어에 대해서 likelihood(우도)를 계산하여 BPE를 적용
- 자주 나오는 단어에 대해서는 underbar없이 토큰화한다.
- 자주 나오지 않는 단어에 대해서는 underbar(_)를 통해 분리한다.
예시
수행하기 이전의 문장: Jet makers feud over seat width with big orders at stake
WordPiece Tokenizer를 수행한 결과(wordpieces): _J et _makers _fe ud _over _seat _width _with _big _orders _at _stake
BERT 원리
BERT는 위 그림과 같이 여러겹의 Transformer Encoder 구조를 가져와서 매번 hidden state들끼리 self-attention을 통해 서로의 latent 정보를 반영한다.
KoBERT
"KoBERT는 기존 BERT의 한국어 성능 한계를 극복하기 위해 개발되었다. 위키피디아나 뉴스 등에서 수집한 수백만 개의 한국어 문장으로 이루어진 대규모말뭉치(corpus)를 학습하였으며, 한국어의 불규칙한 언어 변화의 특성을 반영하기 위해 데이터 기반 토큰화(Tokenization) 기법을 적용하여 기존 대비 27%의 토큰만으로 2.6% 이상의 성능 향상을 이끌어 냈다.
대량의 데이터를 빠른시간에 학습하기 위해 링 리듀스(ring-reduce) 기반 분산 학습 기술을 사용하여, 십억 개 이상의 문장을 다수의 머신에서 빠르게 학습한다. 더불어, 파이토치(PyTorch), 텐서플루(TensorFlow), ONNX, MXNet을 포함한 다양한 딥러닝 API를 지원함으로써, 많은 분야에서 언어 이해 서비스 확산에 기여하고 있다."
참고. https://sktelecom.github.io/project/kobert/
KoBERT
Korean BERT (Bidirectional Encoder Representations from Transformers)
sktelecom.github.io
위의 글처럼 KoBERT는 BERT의 한국어 성능을 개선하기 위해 SKT에서 개발한 모델이다. 사실 BERT에 이미 한국어로 training된 적이 있어 한국어를 BERT에 사용할 수 있지만 한국어만을 위한 모델이 아니기 때문에 한국어를 입력값으로 지정했을 때 좋은 성능이 나오지 않는 문제점이 있었고 이를 개선하기 위해 KoBERT가 제안된 것이다.
KoBERT 코드 분석
KoBERT 모델을 돌릴 때 가장 어려웠던 부분이 Dataset을 정의하는 과정이었다.
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
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))
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)
BERT dataset에서는 word-piece tokenizer와 일정한 크기로 vector를 만들어주기 위해 padding하는 과정도 포함되어있다.
gluonnlp 라이브러리에서 사용되는 nlp 모듈에서 BERTSentenceTransform, BERTTokenizer Class가 가장 BERT 데이터 셋을 구성하는데에 중요했다.
먼저 KoBERT의 tokenizer(word-piece tokenizer)와 vocab을 가져와 gluonnlp의 tokenizer로 변경시켜준뒤 (BERTSPTokenizer 부분) 생성된 tokenizer를 BERTSentenceTransform Class에 입력으로 넣어 여러 문장이 입력으로 들어왔을 때 이를 index화하는과정을 거쳤다.
결과적으로 아래와 같은 반환값을 얻는다.
- token ids : 각 단어가 vocab에서 위치하는 index(token)
- valid length : 문장에서 실제 단어들이 존재하는 단어의 개수, 즉, 길이
- input token type ids : Attention에서 사용될 masked sequence, sequence 별 id가 부여된다. 예) 첫번째 문장:0, 두번째 문장:1
class BERTClassifier(nn.Module):
def __init__(self,
bert,
hidden_size = 768,
num_classes=7, ##클래스 수 조정##
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)
위의 코드는 KoBERT에 input을 넣어 모델의 forward propogation을 하는 과정이다. 다른 부분은 어려운 것이 없으나 attention mask를 보면 valid length(문장마다 단어가 실제로 존재하는 길이)까지만 1로 설정해주고 padding된 부분만 0으로 남기고 있다.
참고로 dr은 Dropout기법의 노드 제거 비율을 의미한다.
Reference
https://arxiv.org/abs/1810.04805
BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
We introduce a new language representation model called BERT, which stands for Bidirectional Encoder Representations from Transformers. Unlike recent language representation models, BERT is designed to pre-train deep bidirectional representations from unla
arxiv.org
https://moondol-ai.tistory.com/241
KoBERT 쉽게 따라하고 간단한 fine-tuning 하기
"데이터셋에 대한 문의가 많습니다. 해당 데이터셋은 제가 프로젝트의 일환으로 "하이닥"이란 웹 사이트에서 크롤링으로 수집한 것입니다. 아시다시피 제 3자 데이터를 수집한 것을 다시 공유하
moondol-ai.tistory.com