새소식

부스트캠프 AI Tech 4기

[ODQA] 3. MRC 데이터 전처리: prepare_train_features

  • -

라이브러리 및 데이터 로드

import random
import numpy as np
import torch
from datasets import load_dataset, load_metric

# 데이터셋 로드 
datasets = load_dataset("squad_kor_v1") # len(datasets["train"]) == 60407

# 평가지표 로드
metric = load_metric("squad")

 

 

▮ PLM 로드

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 = AutoModelQuestionAnswering.from_pretrained(model_name, config=config)

# 모델 구조 확인
model

(qa_outputs) 부분이 QA 부분이다.

out_features2인 이유는 start, end position을 찾기 위함이다.

 

BertForQuestionAnswering(
  (bert): BertModel(
    (embeddings): BertEmbeddings(
      (word_embeddings): Embedding(119547, 768, padding_idx=0)
      (position_embeddings): Embedding(512, 768)
      (token_type_embeddings): Embedding(2, 768)
      (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
      (dropout): Dropout(p=0.1, inplace=False)
    )
    (encoder): BertEncoder(
      (layer): ModuleList(
        (0): BertLayer(
          (attention): BertAttention(
            (self): BertSelfAttention(
              (query): Linear(in_features=768, out_features=768, bias=True)
              (key): Linear(in_features=768, out_features=768, bias=True)
              (value): Linear(in_features=768, out_features=768, bias=True)
              (dropout): Dropout(p=0.1, inplace=False)
            )
            (output): BertSelfOutput(
              (dense): Linear(in_features=768, out_features=768, bias=True)
              (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
              (dropout): Dropout(p=0.1, inplace=False)
            )
          )
          (intermediate): BertIntermediate(
            (dense): Linear(in_features=768, out_features=3072, bias=True)
            (intermediate_act_fn): GELUActivation()
          )
          (output): BertOutput(
            (dense): Linear(in_features=3072, out_features=768, bias=True)
            (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
            (dropout): Dropout(p=0.1, inplace=False)
          )
        )
		
        ...
        
        (11): BertLayer(
          (attention): BertAttention(
            (self): BertSelfAttention(
              (query): Linear(in_features=768, out_features=768, bias=True)
              (key): Linear(in_features=768, out_features=768, bias=True)
              (value): Linear(in_features=768, out_features=768, bias=True)
              (dropout): Dropout(p=0.1, inplace=False)
            )
            (output): BertSelfOutput(
              (dense): Linear(in_features=768, out_features=768, bias=True)
              (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
              (dropout): Dropout(p=0.1, inplace=False)
            )
          )
          (intermediate): BertIntermediate(
            (dense): Linear(in_features=768, out_features=3072, bias=True)
            (intermediate_act_fn): GELUActivation()
          )
          (output): BertOutput(
            (dense): Linear(in_features=3072, out_features=768, bias=True)
            (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
            (dropout): Dropout(p=0.1, inplace=False)
          )
        )
      )
    )
  )
  (qa_outputs): Linear(in_features=768, out_features=2, bias=True)
)

 

▮ 파라미터 설정

# 데이터 전처리를 위한 파라미터
max_seq_length = 384  # Question과 Context, Special token을 합한 Sequence의 max size
pad_to_max_length = True
doc_stride = 128  # Context가 너무 길어서 나누었을 때 오버랩되는 시퀀스 길이
# 학습을 위한 파라미터
max_train_samples = 16
max_val_samples = 16 

preprocessing_num_workers = 4
batch_size = 16
num_train_epochs = 30
n_best_size = 20
max_answer_length = 30

 

 

▮ 데이터 전처리 : prepare_train_features

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

 

Tokenizer의 Input과 Output

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",
    )

💬 return_overflowing_tokens

길이를 넘어가는 토큰들을 반환할 것인지

💬 return_offsets_mapping

각 토큰에 대해 (char_start, char_end) 정보를 반환한 것인지

 

◾ Input

◾ Output

{ 'input_ids' , 'token_type_ids', 'attention_mask', 'offset_mapping', 'overflow_to_sample_mapping' }

 

Context는 max_seq_length 길이만큼 잘리며, doc_stride만큼 겹치게 된다.

위의 데이터를 tokenizing하면 'Question + Context 앞부분' , 'Question + Context 뒷부분' 형태로 2개가 반환될 것이다.

len(tokenized_examples["input_ids"]) → 2

 

💡 offset_mapping

원본 Context에서 각 token의 시작점과 끝점을 tuple 형태로 반환해준 정보

offset_mapping = tokenized_examples.pop("offset_mapping")

 

 

💡 overflow_to_sample_mapping

example(데이터 하나)가 위 그림처럼 max_seq_length를 초과하여 2개로 쪼개질 경우쪼개진 것[0]과 쪼개진 것[1]은 원래 examples[0]번째의 context임을 나타낸다.

overflow_to_sample_mapping = tokenized_examples.pop("overflow_to_sample_mapping")
>>> [0, 0]

 

 

⭐ for 문 뜯어보기

# 쪼개진 문장들만큼 돌겠다
for i, offsets in enumerate(offset_mapping):
    input_ids = tokenized_examples["input_ids"][i]
    cls_index = input_ids.index(tokenizer.cls_token_id)
    
    # 384개의 토큰(max_seq_length)이 question인지 context인지에 대한 정보
    sequence_ids = tokenized_examples.sequence_ids(i) # 101:[CLS] 102:[SEP]
print(input_ids)
>>> [101, 9318, 78136, 70162, 11018, 8905, 119351, 10459, 9901, 89108, 101825, 9642, 11664, 9294, 119137, 10622, 9511, 11664, 13764, 9965, 11018, 11287, 136, 102, 16221, ... ]

print(cls_index) # [CLS] 토큰의 위치 인덱스
>>> 0

print(sequence_ids) # 0은 Question, 1은 문장, None은 special token
>>> [None, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, None, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ... ]

print(f"special token ids : {tokenizer.all_special_ids}")
>>> special token ids : [100, 102, 0, 101, 103] 
print(f"special tokens : {tokenizer.all_special_tokens}")
>>> special tokens : ['[UNK]', '[SEP]', '[PAD]', '[CLS]', '[MASK]']

 

  •  oveflow_to_ample_mapping
# 쪼개진 문장들만큼 돌겠다
for i, offsets in enumerate(offset_mapping):
    input_ids = tokenized_examples["input_ids"][i]
    cls_index = input_ids.index(tokenizer.cls_token_id)
    
    # 384개의 토큰(max_seq_length)이 question인지 context인지에 대한 정보
    sequence_ids = tokenized_examples.sequence_ids(i) # 101:[CLS] 102:[SEP]
    
    # 🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥
    # 하나의 Context가 길이가 길어져서 쪼개져도 answer는 같다
    # overflow_to_sample_mapping을 이용해 context_1과 context_2가 같은 example이라는 것을 표시해주고
    # 쪼개진 context에 맞춰서 정답지를 만들어주기 위해 example_index를 설정
    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])

 

 

  • token_start_index, token_end_index : context의 시작과 끝 부분 index
# 쪼개진 문장들만큼 돌겠다
for i, offsets in enumerate(offset_mapping):
    input_ids = tokenized_examples["input_ids"][i]
    cls_index = input_ids.index(tokenizer.cls_token_id)
    
    # 384개의 토큰(max_seq_length)이 question인지 context인지에 대한 정보
    sequence_ids = tokenized_examples.sequence_ids(i) # 101:[CLS] 102:[SEP]
    
    # 하나의 Context가 길이가 길어져서 쪼개져도 answer는 같다
    # overflow_to_sample_mapping을 이용해 context_1과 context_2가 같은 example이라는 것을 표시해주고
    # 쪼개진 context에 맞춰서 정답지를 만들어주기 위해 example_index를 설정
    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])
    
    # 🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥🐥
    # sequence_ids는 0이면 Question, 1이면 Context였으므로 이를 이용해 context의 시작과 끝 부분의 index를 구함
    token_start_index = 0
    while sequence_ids[token_start_index] != 1:
        token_start_index += 1
    
    token_end_index = len(input_ids) - 1
    while sequence_ids[token_end_index] != 1:
        token_end_index -= 1

 

  • Answer가 Context의 span을 벗어났는지 확인
  • Context 내에 answer가 없을 경우 start_position과 end_position을 [CLS]의 index(0)로 설정
  # context내에 answer가 없을 경우 정답지에 [CLS]의 index를 설정
  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)

 

  • Answer가 Context 내부에 있다면 token_start_index와 token_end_idx를 answer의 시작과 끝 idx로 설정
else:
    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)

 

 

▮ 전처리한 새로운 train_dataset 생성

# 학습 시간 때문에 그냥 전체 train dataset을 사용하지 않고 sampling된 데이터만 사용
train_dataset = train_dataset.select(range(max_train_samples))
column_names = datasets["train"].column_names # 새로운 train_dataset을 만들어 줄 것이므로 기존 칼럼은 삭제

train_dataset = train_dataset.map(
    prepare_train_features,
    batched=True,
    num_proc=preprocessing_num_workers,
    remove_columns=column_names,
    load_from_cache_file=True,
)

 

▮ eval dataset도 생성

# validation을 위한 데이터 준비
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
# 전체 데이터로 평가
eval_examples = datasets["validation"]

eval_dataset = eval_examples.map(
    prepare_validation_features,
    batched=True,
    num_proc=preprocessing_num_workers,
    remove_columns=column_names,
    load_from_cache_file=True,
)

 

728x90
Contents