(BERT) BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding 리뷰 (feat. SQuAD fine-tuning Code)
Google Machine Learning Bootcamp 2022 에서 "NLP 논문 리뷰 스터디" 에 참여하며 정리한 자료입니다
BERT 이전까지, GPT를 포함한 Language model 기반의 사전학습 모델이 좋은 성능을 보여주었다.
이러한 사전학습 모델의 representation 을 downstream task 에 사용하는 방식은 선행연구에서의 모델들로 나누자면, 2가지로 나눌 수 있다.
공통점은 두 가지에 해당되는 모델 모두 language modeling 을 objective 로 쓰기에, unidirectional 한 representation 을 학습한다는데 있다. (self-attention 연산 시 previous 토큰들만 사용)
그런데 이것은, QA와 같은 token-level task 의 성능을 크게 저하시킬 수 있다.
BERT는 fine-tuning 기반 모델의 이러한 약점을 보완할 수 있다.
기존의 language model objective를 Cloze task 라고도 불리는 “masked language model” (MLM) 으로 pre-train objective를 수정했다. MLM은 문장의 일부를 랜덤하게 마스킹하고, 마스킹 된 토큰에 위치한 원래 토큰을 예측하는 문제이다. 이를 학습하기 위해, 모델은 기준 토큰의 양쪽 문맥을 모두 학습할 수 있다. 또한 “next sentence prediction” (NSP) 을 MLM과 함께 objective 로 사용하여, text-pair representation 얻을 수 있었다.
논문의 contribution 은 아래와 같다.
Pre-training을 통해 좋은 representation 을 얻는 방법에 대한 연구들은 word-level에서 sentence-level 로 이어져왔다. 대표적인 예가 ELMo 인데, Left to Right, Right to Left 방향으로의 contextual feature를 concat 함으로써, 양방향의 sentence-level contextual representation 을 얻을 수 있었다. 이는 task-specific architecture를 가지는 모델에서 feature로 사용되므로 ELMo는 feature based 방법에 속한다.
MLM 또한 선행연구에서 LSTM과 함께 쓰이거나 text generation 에 활용되었으나, deep bidirectional 하지는 않았다. 이는 모든 layer에서 bidirectional 하게 학습하지는 못했다는 것을 의미한다.
이전에는 Unlabeled 데이터셋에 대한 사전학습을 통해 word-level의 representation 을 얻고, 이를 모델의 embedding vector로 사용하는, 이른바 feature based 방식이 존재했다.
최근에는 GPT와 같이, sentence-level의 contextual representation 을 Unlabeled 데이터셋의 사전학습으로부터 얻고, downstream task 에서 fine-tuning 진행하는 방식을 사용하고 있다. fine-tuning에서는 적은 수의 파라미터만 새로 학습하면 되어 속도가 빠르다는 장점이 있다. (random initialize 후에 학습되는 파라미터가 소수이기 때문이다)
NLP의 NLI, 기계번역, CV의 Image Classfication 영역의 선행연구들을 통해, 대량의 데이터셋을 사전학습한 모델로 downstream task에 transfer learning 을 수행했을 때, 더 나은 성능을 보이는 것을 알 수 있다.
pre-training + fine-tuning : BERT는 unlabeled 데이터를 pre-train한 모델을 각각의 downstream task에 맞게 fine-tuning 시키는 과정을 가진다.
특히 BERT는 모든 task의 fine-tuning에 동일구조를 사용하는 장점이 있다. 각각의 task에서 Classification layer 만을 추가로 더하면 되기 때문에 minimal 한 수정이 요구된다고도 할 수 있다.
BERT는 Transformer encoder 기반의 구조를 가진다. L은 layer의 수, H는 hidden size, A는 attention head의 수를 나타내며, 논문에서는 모델의 크기가 다른 두가지의 BERT를 제시한다.
downstream task 에 적용할 때, a single sentence 나 a pair of sentences 의 입력이 필요한데, BERT에서는 이를 일관되게 하나의 token sequence로 사용한다는 특징이 있다.
Wordpiece를 통해 30,000 token 의 vocab을 학습했다.
입력을 구성할 때는 항상 special classification token ([CLS])이 첫번째가 되며, Classfication 에서는 [CLS] 위치의 값을 예측에 사용한다. 여러 문장을 넣을 경우, 문장 간에 [SEP] 토큰을 넣어 문장을 구분해준다.
input sequence에 대한 최종적인 embedding vector는 아래 3개의 vector을 summation 하여 생성된다.
Input sequence 의 일부를 random하게 masking하고, masking 된 원래의 token 을 예측하는 문제를 사전학습의 objective로 설정하였다. 실험을 통해 sequence 당 masking 비율은 15% 를 사용하였다.
그러나, 15%을 모두 [mask] 로 대체하는 것은 아니다. 그 이유는, fine-tuning 에서는 [mask] 토큰이 사용되지 않으므로 mismatch 에 의해 성능저하 가능성이 존재하기 때문이다. 따라서, 이를 완화하기 위해 input sequence token 들의 15% 에 대해 8:1:1의 비율로 “[mask]” 토큰으로 대체 (MASK), random token으로 대체 (RND), 원래 token을 유지 (SAME) 하는 방법을 적용했다.
하나의 pair 당 15%만을 예측에 사용하므로, Left to Right (LTR) 모델보다 학습 속도가 느렸으나(converge marginally slower), 이를 감수할만큼 효과는 유의미하였다.
여러 문장을 다루는 sentence-level 의 task에서 language modeling objective는 문장 간 관계에 대한 맥락을 직접 학습하지 못한다. 이를 보완하기 위해 NSP objective 가 추가되었다.
50% 는 이어지는 두 개의 문장으로 sequence를 구성하여 “IsNext” 라는 Label 을 갖게하고, 50% 는 두개의 문장을 random하게 구성하여 “NotNext” 라는 Label을 가지게 함으로써 문장의 연속성을 학습하고 예측하도록 했다. 의도대로 Question Answering (QA), Natural Language Inference(NLI) 에서 NSP가 성능향상을 견인했다는 것을 보여준다. (section 5.1)
# NSP 학습 pair 예시
Input = [CLS] the man went to [MASK] store [SEP]
he bought a gallon [MASK] milk [SEP]
Label = IsNext
Input = [CLS] the man [MASK] to the store [SEP]
penguin [MASK] are flight ##less birds [SEP]
Label = NotNext
선행연구에서 NSP 를 사전학습의 objective로 활용했으나, sentence embedding만을 downstream task 에서 사용했다. BERT의 경우 사전학습 모델의 모든 파라미터를 활용한다.
BERT-BASE, BERT-LARGE의 11개 NLP Task 에 대한 결과를 공유한다.
NLU 성능을 평가하는 데이터셋이며, 학습 진행 detail은 다음과 같다
GLUE 평가결과
# QA 후처리 구현 code
output = model(torch.tensor([input_ids]), token_type_ids=torch.tensor([segment_ids]))
#reconstructing the answer
answer_start = torch.argmax(output.start_logits)
answer_end = torch.argmax(output.end_logits)
if answer_end >= answer_start:
answer = tokens[answer_start]
for i in range(answer_start+1, answer_end+1):
if tokens[i][0:2] == "##":
answer += tokens[i][2:]
else:
answer += " " + tokens[i]
if answer.startswith("[CLS]"):
answer = "Unable to find the answer to your question."
SQuAD v1.1 결과
SQuAD 1.0 에 비해 짧은 answer 이 없고 “답이 없음” 또한 answer 로 포함한다. 이를 구현하기 위해 answer span 에 [CLS] 를 포함하며,
Pre-train $k$ step 에 따른 MNLI Dev accuracy 변화를 통해 사전학습모델의 훈련 step 수의 영향을 분석했다
MLM에서의 masking 기법 변화에 따른 성능 변화를 관찰했다
BERT는 Deep bidirectional architectures 가진 unsupervised pre-training 모델로, 데이터가 적은 task를 포함한 여러 NLP task 에서 SOTA를 달성했다.
BERT | GPT | |
Pre-train Data | BooksCorpus (800M words) + Wikipedia (2,500M words) |
BooksCorpus (800M words) |
Usage of [SEP], [CLS] | pre-train + fine-tuning | fine-tuning |
Train size | 128,000 words | 32,000 words |
Learning Rate | task-specific fine-tuning learning rate | 5e-5 |
Description | Task | Label | |
MNLI | Multi-Genre Natural Language Inference; large-scale if second sentence is entailment, contradiction, or neutral |
Classification | Multi |
QQP | Quora Question Pairs; if two are semantically equivalent | Classification | binary |
QNLI | Question Natural Language Inference; if (question,sentence) pairs contain the correct answer or not |
Classification | binary |
SST-2 | The Stanford Sentiment Treebank; sentiment of movie reviews (single-sentence) |
Classification | binary |
CoLA | The Corpus of Linguistic Acceptability; if linguistically “acceptable” or not |
Classification | binary |
STS-B | Semantic Textual Similarity Benchmark; news headlines and other sources |
Regression | 1 to 5 |
MRPC | Microsoft Research Paraphrase Corpus; online news sources; if two are semantically equivalent |
Classification | binary |
RTE | Recognizing Textual Entailment, small | Classification | binary |
WNLI | Winograd NLI, small (BERT는 평가에서 제외) |
Q. SQuAD fine-tuning 에서의 정확한 input / output 구성은 어떻게 될까?
A : input sequence 는 {context; question} 이고, label 은 start token의 위치와 end token의 위치로, 두개가 된다. 코드를 통해 살펴보자.
Input 을 먼저 살펴보자. Input 은 지문과 질문을 concat한 sequence 가 된다. 이를 tokenizing 후 max_len 길이까지 padding 을 하면 된다. segment_embedding_vector는 지문과 질문을 구분할 수 있도록 생성한다. attention_mask 는 패딩을 제외한, 지문과 질문까지만 1의 값을 갖도록 한다. (나머지는 0)
def preprocess(self):
tokenized_context = tokenizer(context, return_offsets_mapping=True)
tokenized_question = tokenizer(question)
...
# Input sequence : context(지문)와 question(질문)의 concat
input_ids = tokenized_context.ids + tokenized_question.ids[1:]
token_type_ids = [0] * len(tokenized_context.ids) + [1] * len(tokenized_question.ids[1:])
attention_mask = [1] * len(input_ids)
padding_length = max_seq_length - len(input_ids)
# max_len까지 padding 수행
if padding_length > 0:
input_ids = input_ids + ([0] * padding_length)
attention_mask = attention_mask + ([0] * padding_length)
token_type_ids = token_type_ids + ([0] * padding_length)
elif padding_length < 0:
self.skip = True
return
다음은 Label 이다. 데이터셋에서는 answer 로 주어지는 것이 지문 내에 존재하는 단순 텍스트이기 때문에, 해당 answer 의 지문 내 start token 위치와 end token 위치를 찾아서 Label로 사용해야한다. Huggingface 의 transformers 에서 return_offsets_mapping 인자를 사용하면, 분절된 토큰 각각에 대해 지문에서의 character idx 범위를 반환하기 때문에 작업이 편리해진다.
train_text = "I love machine learnining, specifically NLP:)"
tokenized = tokenizer(train_text,
return_offsets_mapping=True) # 각 token의 char_idx 범위 반환
print(tokenized)
print(tokenizer.convert_ids_to_tokens([101, 102])) # [CLS], [SEP]
print(tokenizer.tokenize(train_text))
print(tokenized.offset_mapping) # (start_charidx, end_char_idx) of token
"""
{'input_ids': [101, 146, 1567, 3395, 3858, 16534, 117, 4418, 21239, 2101, 131, 114, 102], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 'offset_mapping': [(0, 0), (0, 1), (2, 6), (7, 14), (15, 20), (20, 25), (25, 26), (27, 39), (40, 42), (42, 43), (43, 44), (44, 45), (0, 0)]}
['[CLS]', '[SEP]']
['I', 'love', 'machine', 'learn', '##ining', ',', 'specifically', 'NL', '##P', ':', ')']
[(0, 0), (0, 1), (2, 6), (7, 14), (15, 20), (20, 25), (25, 26), (27, 39), (40, 42), (42, 43), (43, 44), (44, 45), (0, 0)]
"""
offset_mapping을 활용하면, 아래의 전처리 코드를 통해 지문 내 answer 의 start token 위치와 end token 위치로 이루어진 Label 을 준비할 수 있다.
def preprocess():
tokenized_context = tokenizer(context, return_offsets_mapping=True)
tokenized_question = tokenizer(question)
...
# context(지문)에 대해 char 단위의 배열을 생성하여, 정답인 부분만 1로 표시 (나머지는 0)
is_char_in_ans = [0] * len(context)
for idx in range(self.start_char_idx, end_char_idx):
is_char_in_ans[idx] = 1
ans_token_idx = []
# token의 char 범위 순회하며 정답에 속하면, 지문 내 해당 token의 순서를 저장
for idx, (start, end) in enumerate(tokenized_context.offset_mapping):
if sum(is_char_in_ans[start:end]) > 0:
ans_token_idx.append(idx)
if len(ans_token_idx) == 0:
self.skip = True
return
# start_token_idx : answer 첫번째 token의, 지문 내 token 순서
# end_token_idx : answer 마지막 token의, 지문 내 token 순서
self.start_token_idx = ans_token_idx[0]
self.end_token_idx = ans_token_idx[-1]
Q. (Introduction) 왜 bidirectional representation 의 한계가 sentence-level task와 달리 token-level task 에 악영향을 줄까?
A : 앞에 참조할 수 있는 단어의 개수가 적고/많음의 차이가 있기 때문이다.
unidirictional 하게 학습할 경우 특정 token 의 이전 token 들만 참조하게 되는데, sentence-level task에서 token-level task 보다 input sequence 길이와 정보를 많이 가질 가능성이 크므로, 앞쪽에서 참조할 정보가 더 많을 가능성이 크다.
그러므로 token-level task에서 unidirectional 할 때의 성능 차이가 더 크다고 볼 수 있다.
Q. (5.3 Ablation Study - Feature-based) 에서 We use the representation of the first sub-token as the input to the token-level classifier over the NER label set. 의 의미는?
A : NER에서는 하나의 word 당 하나의 tag 를 예측하는 many-to-many task 인데, tag 에 속한 한 단어가 여러 token으로 이루어지기에 이를 sub-token 으로 표현하였다. 그래서 해당문장이 의미하는 바는, '문장에 속한 여러 단어의 tag들을 예측하기 위해서, 각 word를 이루는 첫번째 token 에 대한 representation 들만을 classifier 에 input 으로 사용한다' 라고 이해할 수 있다.
input : ['BE', '##RT', 'is', 'bid', '##irect', '##ional', 'language', 'model']
label : ["noun", "X", "verb", "ad", "X", "X", "noun", "noun"]
BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
(StackExchange) "What should be the labels for subword tokens in BERT for NER task?" :
(Medium) QA fine-tuning with BERT :
댓글 영역