(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?