(2강) Extraction-based MRC
1.Extraction-based MRC
문제 정의

답변을 생성해내는 것이 아니라 지문에서 찾을 수 있게된다.
이러한 데이터셋은 직접 해당 사이트에서 다운받을 수도 있지만 huggingface의 datasets에서 다운이 가능하다.
평가 방법

왼쪽은 SQuAD 데이터셋이며 오른쪽은 LG에서 SQuAD를 토대로 만든 KorQuAD이다.
F1 점수가 EM 점수보다 높은 것을 알 수 있다.
EM은 예측값과 정답이 캐릭터 단위로 완전히 똑같을 경우에만 1점을 부여하며, 하나라도 다르면 0점을 부여한다.
반면 F1은 예측값과 정답의 overlap을 비율로 계산하며 0점과 1점 사이의 부분점수를 받을 수 있다.
좀 더 자세히는 다음과 같다.

이 때 G.T가 여러개이므로 각각의 G.T와 예측을 비교하게 되고 가장 최고의 점수를 F1 Score로 지정한다.
Overview

지문과 질문이 각각 임베딩되어 모델에 들어가게 되고 모델은 특정 시퀀스를 반환하는 것이 아니라 답이라고 예측되는 토큰의 포지션을 반환한다.
2.Pre-processing
Tokenization
여기서는 OOV 문제를 해결해주고 정보학적으로 이점을 가진 BPE를 사용할 것이며 이 중 WordPiece Tokenizer를 사용한다.
OOV : Out-Of-Vocaburary
BPE : Byte Pair Encoding
Special Tokens
[CLS] 질문 [SEP] 지문
의 꼴로 모델에 입력된다.
Attention Mask
입력 시퀀스 중에서 attention 연산을 할 때 무시할 토큰을 표시한다. 0은 무시, 1은 연산에 포함한다. [PAD]와 같이 의미가 없는 특수토큰을 무시하기 위해 사용한다.
Token Type IDs

질문에는 0, 지문에는 1을 주며 PAD에는 편의상 0을 준다.
모델 출력값

만약, 정답이 '미국 육군 부 참모 총장' 이라면 84와 88을 반환하게 된다. 근데 만약 토큰화가 '저미국', '육군', '부', '참모총', '장이' 로 되어있으면 어떻게 할까? 이 때는 점수가 좀 낮아질 수는 있겠지만 그래도 최소 SPAN을 잡았다는 것으로 인지하고 그대로 이 토큰을 사용하게 된다.
3.Fine-tuning

start, end token이라고 예상되는 벡터는 해당 포지션이 진짜 시작, 끝 token일 확률이며 이를 실제 답의 start/end 위치와 cross-entropy loss를 통해 학습한다.
4.Post-processing
불가능한 답 제거하

질문에서 답이 있을 경우 예측한 context를 벗어날 수 있다.
최적의 답안 찾기

실습
Requirements
!pip install datasets==1.4.1
!pip install transformers==4.4.1
# To use utility functions defined in examples.
!git clone https://github.com/huggingface/transformers.git
import sys
sys.path.append('transformers/examples/question-answering')
from datasets import load_dataset
datasets = load_dataset("squad_kor_v1")
7 : 특정 폴더에 있는 특정 파이썬 코드들을 현재 환경에 import 할 수 있다.
데이터 및 평가 지표 불러오기
from datasets import load_dataset
datasets = load_dataset("squad_kor_v1")
from datasets import load_metric
metric = load_metric('squad')
dataset은 train과 valid로 이루어져 있으며 dataset의 각각의 요소 id, title, context, question, answers로 이루어져 있다.
dataset['train']
으로 train dataset에 접근할 수 있으며dataset['train'][0]
으로 특정 example에 접근할 수 있다.
Pre-trained 모델 불러오기
from transformers import (
AutoConfig,
AutoModelForQuestionAnswering,
AutoTokenizer
)
model_name = "bert-base-multilingual-cased"
config = AutoConfig.from_pretrained(
model_name
)
tokenizer = AutoTokenizer.from_pretrained(
model_name,
use_fast=True
)
model = AutoModelForQuestionAnswering.from_pretrained(
model_name,
config=config
)
config와 tokenizer 그리고 model을 Auto series library를 통해 불러올 수 있다.
설정하기
max_seq_length = 384 # 질문과 컨텍스트, special token을 합한 문자열의 최대 길이
pad_to_max_length = True
doc_stride = 128 # 컨텍스트가 너무 길어서 나눴을 때 오버랩되는 시퀀스 길이
max_train_samples = 16
max_val_samples = 16
preprocessing_num_workers = 4
batch_size = 4
num_train_epochs = 2
n_best_size = 20
max_answer_length = 30
max_seq_length를 설정해야 모델의 size도, pad도 정할 수 있다.
pad_to_max_length=True는 남은 시퀀스를 pad로 채우겠다는 것
doc_stride
max_train_samples, max_val_samples : 학습 및 검증할 데이터 수를 정해놓는다. 여기서는 간단한 테스트 용이기 때문에 작게 정했지만 실제로는 매우 큰 수가 입력된다.
preprocessing_num_workes : 4이상으로는 보통 필요가 없는 경우가 많으며 하드웨어에 dependent 하다.
n_best_size : 답변 길이의 최적의 길이를 설정한다.
max_answer_length : 너무 긴 답변이 나오지 않도록 조절한다.
전처리하기
def prepare_train_features(examples):
# 주어진 텍스트를 토크나이징 한다. 이 때 텍스트의 길이가 max_seq_length를 넘으면 stride만큼 슬라이딩하며 여러 개로 쪼갬.
# 즉, 하나의 example에서 일부분이 겹치는 여러 sequence(feature)가 생길 수 있음.
tokenized_examples = tokenizer(
examples["question"],
examples["context"],
truncation="only_second", # max_seq_length까지 truncate한다. pair의 두번째 파트(context)만 잘라냄.
max_length=max_seq_length,
stride=doc_stride,
return_overflowing_tokens=True, # 길이를 넘어가는 토큰들을 반환할 것인지
return_offsets_mapping=True, # 각 토큰에 대해 (char_start, char_end) 정보를 반환한 것인지
padding="max_length",
)
# example 하나가 여러 sequence에 대응하는 경우를 위해 매핑이 필요함.
overflow_to_sample_mapping = tokenized_examples.pop("overflow_to_sample_mapping")
# offset_mappings으로 토큰이 원본 context 내 몇번째 글자부터 몇번째 글자까지 해당하는지 알 수 있음.
offset_mapping = tokenized_examples.pop("offset_mapping")
# 정답지를 만들기 위한 리스트
tokenized_examples["start_positions"] = []
tokenized_examples["end_positions"] = []
for i, offsets in enumerate(offset_mapping):
input_ids = tokenized_examples["input_ids"][i]
cls_index = input_ids.index(tokenizer.cls_token_id)
# 해당 example에 해당하는 sequence를 찾음.
sequence_ids = tokenized_examples.sequence_ids(i)
# sequence가 속하는 example을 찾는다.
example_index = overflow_to_sample_mapping[i]
answers = examples["answers"][example_index]
# 텍스트에서 answer의 시작점, 끝점
answer_start_offset = answers["answer_start"][0]
answer_end_offset = answer_start_offset + len(answers["text"][0])
# 텍스트에서 현재 span의 시작 토큰 인덱스
token_start_index = 0
while sequence_ids[token_start_index] != 1:
token_start_index += 1
# 텍스트에서 현재 span 끝 토큰 인덱스
token_end_index = len(input_ids) - 1
while sequence_ids[token_end_index] != 1:
token_end_index -= 1
# answer가 현재 span을 벗어났는지 체크
if not (offsets[token_start_index][0] <= answer_start_offset and offsets[token_end_index][1] >= answer_end_offset):
tokenized_examples["start_positions"].append(cls_index)
tokenized_examples["end_positions"].append(cls_index)
else:
# token_start_index와 token_end_index를 answer의 시작점과 끝점으로 옮김
while token_start_index < len(offsets) and offsets[token_start_index][0] <= answer_start_offset:
token_start_index += 1
tokenized_examples["start_positions"].append(token_start_index - 1)
while offsets[token_end_index][1] >= answer_end_offset:
token_end_index -= 1
tokenized_examples["end_positions"].append(token_end_index + 1)
return tokenized_examples
데이터를 사전에 설정했던 대로 전처리해서 dictionary 형태로 반환하게 된다.
train_dataset = datasets["train"]
len(train_dataset)
>>> 60000
train_dataset = train_dataset.select(range(max_train_samples))
len(train_dataset)
>>> 16
select를 이용하면 데이터를 일정 수 만큼 뽑을 수 다.
column_names = datasets["train"].column_names
train_dataset = train_dataset.map(
prepare_train_features,
batched=True,
num_proc=preprocessing_num_workers,
remove_columns=column_names,
load_from_cache_file=True,
)
def prepare_validation_features(examples):
tokenized_examples = tokenizer(
examples['question'],
examples['context'],
truncation="only_second",
max_length=max_seq_length,
stride=doc_stride,
return_overflowing_tokens=True,
return_offsets_mapping=True,
padding="max_length",
)
sample_mapping = tokenized_examples.pop("overflow_to_sample_mapping")
tokenized_examples["example_id"] = []
for i in range(len(tokenized_examples["input_ids"])):
sequence_ids = tokenized_examples.sequence_ids(i)
context_index = 1
sample_index = sample_mapping[i]
tokenized_examples["example_id"].append(examples["id"][sample_index])
tokenized_examples["offset_mapping"][i] = [
(o if sequence_ids[k] == context_index else None)
for k, o in enumerate(tokenized_examples["offset_mapping"][i])
]
return tokenized_examples
valid 데이터도 똑같이 처리 준다.
eval_examples = datasets["validation"]
eval_examples = eval_examples.select(range(max_val_samples))
eval_dataset = eval_examples.map(
prepare_validation_features,
batched=True,
num_proc=preprocessing_num_workers,
remove_columns=column_names,
load_from_cache_file=True,
)
Fine-tuning 하기
from transformers import default_data_collator, TrainingArguments, EvalPrediction
from trainer_qa import QuestionAnsweringTrainer
from utils_qa import postprocess_qa_predictions
default_data_collator : 여러 개의 example을 collate 해주는 역
TrainingArguments : 학습을 할 때 줄 수 있는 config를 한번에 줄 수 있는 편리한 기능
Eval Prediction : 좀 더 편하게 예측할 수 있도록 함
QuestionAnsweringTrainer : 학습을 더 편하게 수 있다.
postprocess_qa_predictions : 결과를 얻고나서 한번 더 post process를 해야하는데 이를 가능하게 해준다.
def compute_metrics(p: EvalPrediction):
return metric.compute(predictions=p.predictions, references=p.label_ids)
training_args = TrainingArguments(
output_dir="outputs",
do_train=True,
do_eval=True,
learning_rate=3e-5,
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
num_train_epochs=num_train_epochs,
weight_decay=0.01,
)
trainer = QuestionAnsweringTrainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
eval_examples=datasets["validation"],
tokenizer=tokenizer,
data_collator=default_data_collator,
post_process_function=post_processing_function,
compute_metrics=compute_metrics,
)
train_result = trainer.train()
train_result
>>> TrainOutput(global_step=12, training_loss=4.897604942321777, metrics={'train_runtime': 224.1287, 'train_samples_per_second': 0.054, 'total_flos': 19604022976512.0, 'epoch': 2.0, 'init_mem_cpu_alloc_delta': 4986, 'init_mem_cpu_peaked_delta': 16725, 'train_mem_cpu_alloc_delta': 160674, 'train_mem_cpu_peaked_delta': 153619})
평가하
metrics = trainer.evaluate()
metrics
>>> {'epoch': 2.0, 'exact_match': 0.0, 'f1': 0.0}
Last updated
Was this helpful?