질의응답

질의응답 (Question answering)

질의응답 작업은 질문이 주어지면 답변을 반환합니다. Alexa, Siri, Google 같은 가상 어시스턴트에게 날씨를 물어본 적이 있다면 이미 질의응답 모델을 사용해 본 것입니다. 질의응답 작업에는 두 가지 일반적인 유형이 있습니다.

출처: 문서

본문

  • 추출형(Extractive): 주어진 문맥에서 답변을 추출합니다.
  • 생성형(Abstractive): 질문에 올바르게 답하는 답변을 문맥에서 생성합니다.

이 가이드에서 다룰 내용은 다음과 같습니다.

  1. 추출형 질의응답을 위해 SQuAD 데이터셋에서 DistilBERT를 파인튜닝합니다.
  2. 파인튜닝된 모델을 추론(inference)에 사용합니다.

이 작업과 호환되는 모든 아키텍처와 체크포인트를 보려면 task-page를 확인하는 것을 권장합니다.

시작하기 전에 필요한 라이브러리를 모두 설치했는지 확인해 주세요.

pip install transformers datasets evaluate

Hugging Face 계정에 로그인해 모델을 커뮤니티에 업로드하고 공유하는 것을 권장합니다. 프롬프트가 나타나면 토큰을 입력해 로그인하세요.

>>> from huggingface_hub import notebook_login

>>> notebook_login()

SQuAD 데이터셋 로드하기

🤗 Datasets 라이브러리에서 SQuAD 데이터셋의 더 작은 하위 집합을 로드하는 것부터 시작하겠습니다. 이렇게 하면 전체 데이터셋으로 훈련하는 데 더 많은 시간을 쓰기 전에 실험해 보고 모든 것이 잘 작동하는지 확인할 수 있습니다.

>>> from datasets import load_dataset

>>> squad = load_dataset("squad", split="train[:5000]")

train_test_split 메서드로 데이터셋의 train 스플릿을 train과 test 세트로 나눕니다.

>>> squad = squad.train_test_split(test_size=0.2)

그런 다음 예시를 하나 살펴봅니다.

>>> squad["train"][0]
{'answers': {'answer_start': [515], 'text': ['Saint Bernadette Soubirous']},
 'context': 'Architecturally, the school has a Catholic character. Atop the Main Building\'s gold dome is a golden statue of the Virgin Mary. Immediately in front of the Main Building and facing it, is a copper statue of Christ with arms upraised with the legend "Venite Ad Me Omnes". Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basilica is the Grotto, a Marian place of prayer and reflection. It is a replica of the grotto at Lourdes, France where the Virgin Mary reputedly appeared to Saint Bernadette Soubirous in 1858. At the end of the main drive (and in a direct line that connects through 3 statues and the Gold Dome), is a simple, modern stone statue of Mary.',
 'id': '5733be284776f41900661182',
 'question': 'To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France?',
 'title': 'University_of_Notre_Dame'
}

여기에는 몇 가지 중요한 필드가 있습니다.

  • answers: 답변 토큰의 시작 위치와 답변 텍스트.
  • context: 모델이 답변을 추출해야 하는 배경 정보.
  • question: 모델이 답해야 하는 질문.

전처리 (Preprocess)

다음 단계는 question과 context 필드를 처리할 DistilBERT 토크나이저를 로드하는 것입니다.

>>> from transformers import AutoTokenizer

>>> tokenizer = AutoTokenizer.from_pretrained("distilbert/distilbert-base-uncased")

질의응답 작업에 특정한 몇 가지 전처리 단계가 있다는 점을 알아두세요.

  1. 데이터셋의 일부 예시는 모델의 최대 입력 길이를 초과하는 매우 긴 context를 가질 수 있습니다. 더 긴 시퀀스를 처리하려면 truncation="only_second"로 설정해 context만 잘라냅니다.
  2. 다음으로, return_offset_mapping=True로 설정해 답변의 시작·끝 위치를 원본 context에 매핑합니다.
  3. 매핑을 확보했으면 이제 답변의 시작·끝 토큰을 찾을 수 있습니다. sequence_ids 메서드를 사용해 오프셋의 어느 부분이 question에 해당하고 어느 부분이 context에 해당하는지 찾습니다.

answer의 시작·끝 토큰을 context에 매핑하고 잘라내는 함수를 만드는 방법은 다음과 같습니다.

>>> def preprocess_function(examples):
...     questions = [q.strip() for q in examples["question"]]
...     inputs = tokenizer(
...         questions,
...         examples["context"],
...         max_length=384,
...         truncation="only_second",
...         return_offsets_mapping=True,
...         padding="max_length",
...     )

...     offset_mapping = inputs.pop("offset_mapping")
...     answers = examples["answers"]
...     start_positions = []
...     end_positions = []

...     for i, offset in enumerate(offset_mapping):
...         answer = answers[i]
...         start_char = answer["answer_start"][0]
...         end_char = answer["answer_start"][0] + len(answer["text"][0])
...         sequence_ids = inputs.sequence_ids(i)

...         # Find the start and end of the context
...         idx = 0
...         while sequence_ids[idx] != 1:
...             idx += 1
...         context_start = idx
...         while sequence_ids[idx] == 1:
...             idx += 1
...         context_end = idx - 1

...         # If the answer is not fully inside the context, label it (0, 0)
...         if offset[context_start][0] > end_char or offset[context_end][1] < start_char:
...             start_positions.append(0)
...             end_positions.append(0)
...         else:
...             # Otherwise it's the start and end token positions
...             idx = context_start
...             while idx <= context_end and offset[idx][0] <= start_char:
...                 idx += 1
...             start_positions.append(idx - 1)

...             idx = context_end
...             while idx >= context_start and offset[idx][1] >= end_char:
...                 idx -= 1
...             end_positions.append(idx + 1)

...     inputs["start_positions"] = start_positions
...     inputs["end_positions"] = end_positions
...     return inputs

이 전처리 함수를 전체 데이터셋에 적용하려면 🤗 Datasets map 함수를 사용하세요. batched=True로 설정하면 데이터셋의 여러 요소를 한 번에 처리해 map 함수를 더 빠르게 할 수 있습니다. 필요하지 않은 컬럼은 모두 제거하세요.

>>> tokenized_squad = squad.map(preprocess_function, batched=True, remove_columns=squad["train"].column_names)

이제 DefaultDataCollator을 사용해 예시 배치를 만듭니다. 🤗 Transformers의 다른 데이터 콜레이터와 달리 DefaultDataCollator은 패딩 같은 추가 전처리를 적용하지 않습니다.

>>> from transformers import DefaultDataCollator

>>> data_collator = DefaultDataCollator()

훈련 (Train)

Trainer로 모델을 파인튜닝하는 방법이 익숙하지 않다면 여기의 기본 튜토리얼을 확인해 보세요!

이제 모델 훈련을 시작할 준비가 되었습니다! AutoModelForQuestionAnswering으로 DistilBERT를 로드합니다.

>>> from transformers import AutoModelForQuestionAnswering, TrainingArguments, Trainer

>>> model = AutoModelForQuestionAnswering.from_pretrained("distilbert/distilbert-base-uncased")

이 시점에서 남은 단계는 세 가지뿐입니다.

  1. TrainingArguments에서 훈련 하이퍼파라미터를 정의합니다. 유일한 필수 매개변수는 모델을 저장할 위치를 지정하는 output_dir입니다. push_to_hub=True로 설정하면 이 모델을 Hub에 푸시합니다(모델을 업로드하려면 Hugging Face에 로그인해야 합니다).
  2. Trainer에 모델, 데이터셋, 토크나이저, 데이터 콜레이터와 함께 훈련 인자를 전달합니다.
  3. train()을 호출해 모델을 파인튜닝합니다.
>>> training_args = TrainingArguments(
...     output_dir="my_awesome_qa_model",
...     eval_strategy="epoch",
...     learning_rate=2e-5,
...     per_device_train_batch_size=16,
...     per_device_eval_batch_size=16,
...     num_train_epochs=3,
...     weight_decay=0.01,
...     push_to_hub=True,
... )

>>> trainer = Trainer(
...     model=model,
...     args=training_args,
...     train_dataset=tokenized_squad["train"],
...     eval_dataset=tokenized_squad["test"],
...     processing_class=tokenizer,
...     data_collator=data_collator,
... )

>>> trainer.train()

훈련이 끝나면 push_to_hub() 메서드로 모델을 Hub에 공유해 모두가 사용할 수 있게 하세요.

>>> trainer.push_to_hub()

질의응답을 위해 모델을 파인튜닝하는 더 심층적인 예시는 해당 PyTorch notebook을 참고하세요.

평가 (Evaluate)

질의응답 평가는 상당한 양의 후처리를 요구합니다. 시간을 너무 많이 들이지 않기 위해 이 가이드에서는 평가 단계를 생략합니다. Trainer는 훈련 중에도 평가 loss를 계산하므로 모델 성능을 전혀 모르는 상태는 아닙니다.

시간이 더 있고 질의응답을 위한 모델 평가 방법이 궁금하다면 🤗 Hugging Face 코스의 Question answering 챕터를 확인해 보세요!

추론 (Inference)

좋습니다. 이제 모델을 파인튜닝했으니 추론에 사용할 수 있습니다!

모델이 예측하길 원하는 질문과 문맥을 생각해 보세요.

>>> question = "How many programming languages does BLOOM support?"
>>> context = "BLOOM has 176 billion parameters and can generate text in 46 languages natural languages and 13 programming languages."

텍스트를 토크나이즈하고 PyTorch 텐서를 반환합니다.

>>> from transformers import AutoTokenizer

>>> tokenizer = AutoTokenizer.from_pretrained("my_awesome_qa_model")
>>> inputs = tokenizer(question, context, return_tensors="pt")

입력을 모델로 전달하고 logits을 얻습니다.

>>> import torch
>>> from transformers import AutoModelForQuestionAnswering

>>> model = AutoModelForQuestionAnswering.from_pretrained("my_awesome_qa_model")
>>> with torch.no_grad():
...     outputs = model(**inputs)

시작·끝 위치에 대해 모델 출력에서 가장 높은 확률을 얻습니다.

>>> answer_start_index = outputs.start_logits.argmax()
>>> answer_end_index = outputs.end_logits.argmax()

예측된 토큰을 디코딩해 답변을 얻습니다.

>>> predict_answer_tokens = inputs.input_ids[0, answer_start_index : answer_end_index + 1]
>>> tokenizer.decode(predict_answer_tokens)
'176 billion parameters and can generate text in 46 languages natural languages and 13'

더 알아보기 (Learn more)